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
Private conversation event await ctx.realtime.channel(conversationEvents, args).publish(event) Declared scope and subscriber authorization protect payload delivery

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 builder 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.

For a resumable import, checkpoint the stages and return a small summary:

// gencow/imports.ts
import { v } from "@gencow/core";
import { workflow } from "./runtime";

export const runImport = workflow
    .name("imports.run")
    .input(v.object({ sourceId: v.string() }))
    .maxDuration("10m")
    .handler(async (wf, { sourceId }) => {
        const prepared = await wf.step("prepare", () => prepareImport(sourceId));
        const saved = await wf.step("save", () => saveImport({
            preparedId: prepared.id,
            idempotencyKey: `${wf.workflowId}:save`,
        }));
        return { importId: saved.id, rowCount: saved.rowCount };
    });

prepareImport() and saveImport() are app-owned helpers. Persist large input data in the preparation stage, and make the save helper honor its idempotency key. Add runImport to defineApi({ workflows: { runImport } }), run gencow codegen, then start api.imports.run and observe its returned run ID.

Advanced — completed steps versus repeated writes: A retry reuses saved checkpoint output. A crash after a write succeeds but before its checkpoint is saved can still repeat that write, so the database or external service must enforce the idempotency key. For more examples, see Workflow Agents and authorized realtime channels.