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 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 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 dev

gencow 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 dev automatically runs bunx drizzle-kit generate to create SQL migration files in gencow/migrations/. The one-shot development command gencow deploy also 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 in gencow/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 --local

Verify 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/react

Point 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.app

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)
bunx gencow@latest db:seed

8. 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 dev watches for file changes and auto-deploys. gencow static deploys your built frontend.

A plain request to deploy means the development target. For production deployment on Personal and Pro plans, use gencow deploy --prod only when production was explicitly requested.

Next Steps