Common Recipes

Copy-paste patterns for auth, CRUD pagination, storage, realtime, and slow work

These recipes are intentionally small. They show the default Gencow path first, then point to deeper guides when a workflow needs more control.

Vite + React + Auth + API Client

Use one base URL for auth and API calls.

// src/lib/gencow.ts
import { createAuthClient, createGencowClient } from "@gencow/client";
import { api } from "../gencow/api";

export const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456";
export const auth = createAuthClient(baseUrl);
export const apiClient = createGencowClient({ api, baseUrl, auth });
export const { signIn, signUp, signOut } = auth;
// src/main.tsx
import { GencowProvider } from "@gencow/react";
import { apiClient } from "./lib/gencow";

root.render(
    <GencowProvider apiClient={apiClient}>
        <App />
    </GencowProvider>
);

Login + CRUD List + Pagination

createCrud().list returns { data, total, nextCursor? }. The default list call should pass an explicit empty args object.

import { useMemo, useState } from "react";
import { useMutation, useQuery } from "@gencow/react";
import { api } from "../gencow/api";

const limit = 10;

export function TaskBoard() {
    const [page, setPage] = useState(1);
    const { data: result, isLoading, isFetching, error } = useQuery(api.tasks.list, { page, limit });
    const { mutate: createTask, isPending } = useMutation(api.tasks.create);

    const totalPages = useMemo(
        () => Math.max(1, Math.ceil((result?.total ?? 0) / limit)),
        [result?.total]
    );

    if (isLoading) return <p>Loading tasks...</p>;
    if (error) return <button onClick={() => setPage(1)}>Retry</button>;

    return (
        <section>
            <button disabled={isPending} onClick={() => createTask({ title: "New task" })}>
                Add task
            </button>

            {result && result.data.length === 0 ? (
                <p>No tasks yet.</p>
            ) : (
                <ul>
                    {result?.data.map((task) => (
                        <li key={task.id}>{task.title}</li>
                    ))}
                </ul>
            )}

            <nav>
                <button onClick={() => setPage((value) => value - 1)} disabled={page <= 1 || isFetching}>
                    Previous
                </button>
                <span>
                    Page {page} of {totalPages}
                </span>
                <button onClick={() => setPage((value) => value + 1)} disabled={page >= totalPages || isFetching}>
                    Next
                </button>
            </nav>
        </section>
    );
}

Storage Preview for Private Files

Store private files by default and return short-lived read grants for browser previews.

export const listFiles = procedure.query
    .name("files.list")
    .handler(async ({ context: ctx }) => {
        const session = ctx.auth.requireAuth();
        const rows = await ctx.db.select().from(files).where(eq(files.userId, session.user.id));

        return Promise.all(
            rows.map(async (file) => ({
                id: file.id,
                name: file.name,
                previewUrl: (await ctx.storage.createReadGrant(file.storageId, {
                    ttlSeconds: 300,
                    disposition: "inline",
                })).url,
            }))
        );
    });

Use public visibility only for intentionally public website assets:

const stored = await ctx.storage.store(file, { visibility: "public" });
const url = await ctx.storage.getPublicUrl(stored.id);

AI Image - Storage

Generate an image through the Gencow AI helper, store it, and return a safe browser URL. Do not log prompts, provider keys, or private grant URLs.

import { ai } from "./ai";
import { procedure } from "./runtime";

export const generateCover = procedure.mutation
    .name("covers.generate")
    .handler(async ({ context: ctx }) => {
        const session = ctx.auth.requireAuth();
        const image = await ai.image.generate({
            model: "gpt-image-2",
            prompt: "A clean cover image for a task dashboard",
            quality: "low",
        });

        const stored = await ctx.storage.storeBuffer(image.bytes, {
            name: "cover.png",
            type: "image/png",
            visibility: "private",
            metadata: { userId: session.user.id },
        });

        return {
            storageId: stored.id,
            previewUrl: (await ctx.storage.createReadGrant(stored.id, { ttlSeconds: 300 })).url,
        };
    });

Realtime Chooser

Use case API Why
Private/RLS list after a custom mutation ctx.realtime.invalidate("tasks.list") Each client refetches with its own JWT and RLS scope
Public aggregate counter ctx.realtime.refresh("stats.public") Server recomputes a safe public result
Public ticker/event ctx.realtime.emit("ticker.feed", payload) Payload is safe for all subscribers

For createCrud() write procedures, realtime invalidation is built in. create invalidates matching list buckets; update and remove also invalidate the exact get({ id }) subscription when get is generated.

Slow Work Chooser

Work Put it in Notes
Fast validation and one DB write procedure.mutation Keep the request short
Webhook or third-party callback httpRoute / httpAction Validate input and secrets at the boundary
Multi-step resumable job workflow() Use stable step names and idempotent side effects
Delayed one-off follow-up ctx.scheduler.runAfter() BaaS uses Platform DB-backed durable scheduling
Fixed recurring work cronJobs Deploy syncs the schedule into platform cron rows

Local development keeps scheduler behavior best-effort and in-memory. Cloud deploy uses Platform DB-backed durable scheduling.