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.

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.