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.