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

npx gencow@latest init my-todo --template task-app
cd my-todo && bun install

2. 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 and createCrud() — 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. Without ownerRls or tableRls, ctx.db queries can return and mutate all rows in the table. Use ctx.db for normal app logic; reserve ctx.unsafeDb for audited admin paths only.

createCrud() layers auth (401 for unauthenticated callers by default), input validation, and realtime invalidation on top of RLS. See gencow/SECURITY.md (scaffolded from the template) for the full checklist — multi-tenant tableRls, storage grants, AI proxy rules, and deploy audit.

⚠️ Schema rule: createCrud() requires an id primary key (serial, text, or uuid). 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 / remove run 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 different prefix
  • ID auto-detection: serial → number, text / uuid → string

task-app template: Ships hand-written procedure handlers in tasks.ts as a reference. You can replace them with createCrud as above for less boilerplate.

Need custom logic? Add procedure.query / procedure.mutation alongside createCrud() — 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 the Dev Server

gencow dev

On first run, gencow dev codegens the typed client API, syncs schema-derived migrations, and starts the local API server with hot reload. You should see output like:

  Gencow Dev

  ▸ Backend root: ./gencow
  ▸ Storage:   ./.gencow/uploads
  ▸ DB:        Cloud PostgreSQL

  ✓ Schema → migrations synced
  ✓ Generated src/gencow/api.ts
  ✓ Generated gencow/README.md (AI vibe-coding guide)

  Starting server with hot-reload...

  ✓ Server running at http://localhost:5456
  ✓ Dashboard: http://localhost:5456/_admin

✓ Schema → migrations syncedgencow dev automatically runs npx drizzle-kit generate to create SQL migration files in gencow/migrations/. gencow deploy (production) also auto-runs this before bundling — so schema.ts changes are always captured without a manual step.

✓ Generated src/gencow/api.ts — This file is generated from the procedures you registered in gencow/index.ts. Do not edit it manually.

Verify the API

Open the admin dashboard at http://localhost:5456/_admin to:

  • 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:

npm install @gencow/client @gencow/react

Set 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 invalidates tasks.list subscribers over WebSocket. Each connected client refetches with its own auth token — no manual refetch() 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)
gencow db:seed

8. Deploy

# Login (first time only)
gencow login

# Start dev (real-time backend deployment)
gencow dev

# Deploy frontend (if you have one)
VITE_API_URL=https://<app-id>.gencow.app npm run build
gencow static dist/

That's it! Your app is live on https://<app-id>.gencow.app. 🎉

gencow dev watches for file changes and auto-deploys. gencow static deploys your built frontend.

For production deployment on Startup and Enterprise plans, use gencow deploy --prod.

Next Steps