Quickstart
Build a frontend + backend Todo app in 5 minutes
Let's build a Todo app with authentication, CRUD operations, and realtime updates — from scratch in 5 minutes.
1. Create the Project
bunx gencow@latest init my-todo --template task-app
cd my-todo && bun install2. Explore the Schema
Open gencow/schema.ts — the template already includes a tasks table:
import { pgTable, serial, text, boolean, timestamp } from "drizzle-orm/pg-core";
import { ownerRls } from "@gencow/core";
import { user } from "./schema-auth";
export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
done: boolean("done").default(false).notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (t) => ownerRls(t.userId));Security: Gencow injects the signed-in user into
ctx.db, so PostgreSQL Row-Level Security (RLS) policies declared in your schema apply automatically in procedures andcreateCrud()— no manual.where(eq(...))for basic owner isolation.
ownerRls(t.userId)is the common preset: it matches each row's owner column to the auth session. WithoutownerRlsortableRls,ctx.dbqueries can return and mutate all rows in the table. Usectx.dbfor normal app logic; reservectx.unsafeDbfor audited admin paths only.
createCrud()layers auth (401 for unauthenticated callers by default), input validation, and realtime invalidation on top of RLS. Seegencow/SECURITY.md(scaffolded from the template) for the full checklist — multi-tenanttableRls, storage grants, AI proxy rules, and deploy audit.⚠️ Schema rule:
createCrud()requires anidprimary key (serial,text, oruuid). This template already includes one.
3. Define the CRUD API
In gencow/tasks.ts, use createCrud() from ./runtime (not @gencow/core):
import { createCrud } from "./runtime";
import { tasks } from "./schema";
export const tasksCrud = createCrud(tasks, { prefix: "tasks" });createCrud(tasks, { prefix: "tasks" }) generates:
- Read procedures:
tasks.list→{ data, total },tasks.get→ single row by id - Write procedures:
tasks.create,tasks.update,tasks.remove— auth + realtime invalidation by default - Security: Authentication required unless you pass
{ allowAnonymous: true } - Access gates:
access.read/create/update/removerun before database access and can deny the operation - Selective exposure: Use
methods: ["list", "get"](or any subset) to generate only the operations you want to expose - Multiple CRUD views: You can call
createCrud()multiple times for the same table, but each one must use a differentprefix - ID auto-detection:
serial→ number,text/uuid→ string
task-app template: Ships hand-written
procedurehandlers intasks.tsas a reference. You can replace them withcreateCrudas above for less boilerplate.Need custom logic? Add
procedure.query/procedure.mutationalongsidecreateCrud()— see Queries Guide and Mutations Guide.
4. Register API Definitions
Open gencow/index.ts — import CRUD definitions and register them with defineApi:
import "./runtime";
import { defineApi } from "@gencow/core";
import { tasksCrud } from "./tasks";
export default defineApi({
crud: {
tasks: tasksCrud,
},
});The server exposes only what you register here. Feature modules are not auto-discovered, and methods controls which generated procedures end up in api.ts.
5. Start Cloud Development
Gencow projects are created and edited locally, but the recommended review target is the cloud Dev app. This is especially important for AI and other platform-managed features; local-only checks do not reproduce the hosted runtime and are not recommended as the complete validation path.
bunx gencow@latest login
bunx gencow@latest devgencow dev uses cloud development by default. It codegens the typed client
API, syncs schema-derived migrations, deploys the backend to the dev app, and
streams logs while watching for changes. You should see the cloud app and
Dashboard URLs in the output:
Gencow Dev (cloud)
▸ Backend root: ./gencow
▸ DB: Cloud PostgreSQL
✓ Schema → migrations synced
✓ Generated src/gencow/api.ts
✓ Generated gencow/README.md (AI vibe-coding guide)
✓ App: https://<app-id>.gencow.app
✓ Dashboard: https://gencow.app/apps/<app-id>
✓ Schema → migrations synced—gencow devautomatically runsbunx drizzle-kit generateto create SQL migration files ingencow/migrations/. The one-shot development commandgencow deployalso runs it before bundling. Production is a separate, explicit command:gencow deploy --prod.
✓ Generated src/gencow/api.ts— This file is generated from the procedures you registered ingencow/index.ts. Do not edit it manually.
Prefer a fully local runtime? Run this instead; it starts Bun, PGlite, local storage, and the Admin Dashboard on your computer:
bunx gencow@latest dev --localVerify the API
Open the cloud Dashboard URL printed by gencow dev. In local mode, open
http://localhost:5456/_admin. You can:
- Browse your database tables
- View registered procedures
- Test API endpoints
6. Connect a Vite + React Frontend
For a new UI, use Vite + React conventions: src/main.tsx as the entry point, src/App.tsx for the app shell, and import.meta.env.VITE_API_URL for the backend URL. Do not choose Next.js unless the user explicitly asks for Next.js/SSR or you are working inside an existing Next.js project.
Install the React SDK:
bun add @gencow/client @gencow/reactPoint the frontend at the cloud dev app in the project-root .env. If you chose
local mode, use http://localhost:5456 instead.
VITE_API_URL=https://<app-id>.gencow.appSet Up Auth
// src/lib/auth.ts
import { createAuthClient } from "@gencow/client";
const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456";
export const auth = createAuthClient(baseUrl);
export const { signIn, signUp, signOut } = auth;
Create the runtime client
// src/lib/apiClient.ts
import { createGencowClient } from "@gencow/client";
import { api } from "../gencow/api";
import { auth } from "./auth";
const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456";
export const apiClient = createGencowClient({ api, baseUrl, auth });Set Up the Provider
// src/main.tsx
import { GencowProvider } from "@gencow/react";
import { apiClient } from "./lib/apiClient";
function AppWrapper({ children }) {
return (
<GencowProvider apiClient={apiClient}>
{children}
</GencowProvider>
);
}Build the Todo UI
import { useQuery, useMutation } from "@gencow/react";
import { api } from "./gencow/api"; // auto-generated by `gencow dev`
export default function App() {
// useQuery returns { data, isLoading, error, ... }
// createCrud().list returns { data: Task[], total: number }
const { data: result, isLoading, error } = useQuery(api.tasks.list, {});
const { mutate: createTask, isPending: isCreating } = useMutation(api.tasks.create);
const { mutate: updateTask } = useMutation(api.tasks.update);
const { mutate: removeTask } = useMutation(api.tasks.remove);
if (isLoading && !result) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>My Todos ({result?.total ?? 0})</h1>
{/* Create form */}
<form onSubmit={(e) => {
e.preventDefault();
const title = new FormData(e.currentTarget).get("title") as string;
createTask({ title });
e.currentTarget.reset();
}}>
<input name="title" placeholder="New task..." required />
<button disabled={isCreating}>
{isCreating ? "Adding..." : "Add"}
</button>
</form>
{/* Task list — use result.data (not result directly) */}
<ul>
{result?.data.map((t) => (
<li key={t.id}>
<span
onClick={() => updateTask({ id: t.id, done: !t.done })}
style={{ cursor: "pointer" }}
>
{t.done ? "✅" : "⬜"} {t.title}
</span>
<button onClick={() => removeTask({ id: t.id })}>🗑️</button>
</li>
))}
</ul>
</div>
);
}Realtime sync: When you call
createTask(), the server invalidatestasks.listsubscribers over WebSocket. Each connected client refetches with its own auth token — no manualrefetch()needed. Authenticated queries are not pushed with full row payloads (that would leak data); only public/anonymous endpoints may receive direct pushes.
7. Add Seed Data (Optional)
Create gencow/seed.ts to populate the database with test data:
import { tasks } from "./schema";
export default async function seed(ctx) {
// Create a test user first (auth is handled separately)
// Then seed tasks
await ctx.db.insert(tasks).values([
{ title: "Learn Gencow", userId: "test-user-id" },
{ title: "Build an awesome app", userId: "test-user-id" },
{ title: "Deploy to cloud", userId: "test-user-id" },
]);
}# Run seed (server must be running)
bunx gencow@latest db:seed8. Deploy
# Deploy frontend (if you have one)
VITE_API_URL=https://<app-id>.gencow.app bun run build
bunx gencow@latest static dist/That's it! Your app is live on https://<app-id>.gencow.app. 🎉
gencow devwatches for file changes and auto-deploys.gencow staticdeploys your built frontend.A plain request to deploy means the development target. For production deployment on Personal and Pro plans, use
gencow deploy --prodonly when production was explicitly requested.
Next Steps
- Project Structure — Understand every config file
- Schema Guide — Advanced schema patterns
- Authentication — Auth setup in detail
- AI Engine — Add AI to your app
- Deployment — Cloud deployment details