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 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 "./generated/auth-schema";

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)]);

Security: pgTable with ownerRls() applies PostgreSQL Row-Level Security to automatically isolate data per user. Combined with createCrud(), every operation enforces authentication and data ownership — no manual filtering needed.

⚠️ Schema Rule: Every table using createCrud() must have an id column. The template already includes this.

3. Auto-Generated CRUD API

Open gencow/tasks.ts — a single line generates the entire API:

import { createCrud } from "./runtime";
import { tasks } from "./schema";

// ✅ One line → list, get, create, update, remove with auth + realtime
export const { list, get, create, update, remove } = createCrud(tasks);

createCrud(tasks) auto-generates:

  • Read procedures: tasks.list → returns { data: Task[], total: number }, tasks.get → returns single task by id
  • Write procedures: tasks.create, tasks.update, tasks.remove — all with auth + realtime push
  • Security: Authentication required by default. Use createCrud(tasks, { allowAnonymous: true }) for public access.
  • ID auto-detection: serial → number, text/uuid → string (automatic)

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 the generated procedure definitions and register them with defineApi:

import "./runtime";

import { defineApi } from "@gencow/core";
import {
    list as tasksList,
    get as tasksGet,
    create as tasksCreate,
    update as tasksUpdate,
    remove as tasksRemove,
} from "./tasks";

export default defineApi({
    procedures: {
        tasksList,
        tasksGet,
        tasksCreate,
        tasksUpdate,
        tasksRemove,
    },
});

defineApi({ procedures }) is the current registration source of truth. Do not use namespace re-exports or defineApi({ crud }) in new app code.

5. Start the Dev Server

gencow dev

You'll see:

  Gencow Dev

  ▸ Functions: ./gencow
  ▸ Storage:   ./.gencow/uploads
  ▸ DB:        Cloud PostgreSQL

  ✓ Schema → migrations synced
  ✓ Generated 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.

Verify the API

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

  • Browse your database tables
  • View registered queries and mutations
  • 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/react";
export const auth = createAuthClient();
export const { signIn, signUp, signOut, useAuth } = auth;

Set Up the Provider

// src/main.tsx
import { GencowProvider } from "@gencow/react";
import { auth } from "./lib/auth";

function AppWrapper({ children }) {
    // VITE_API_URL is auto-set by `gencow init` and `gencow dev`
    const baseUrl = import.meta.env.VITE_API_URL;

    return (
        <GencowProvider baseUrl={baseUrl} auth={auth}>
            {children}
        </GencowProvider>
    );
}

Build the Todo UI

import { useQuery, useMutation } from "@gencow/react";
import { api } from "../gencow/api";  // auto-generated

function TodoApp() {
    // createCrud().list returns { data: Task[], total: number }
    const result = 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 (result === undefined) return <div>Loading...</div>;

    return (
        <div>
            <h1>My Todos ({result.total})</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 magic: When you call createTask(), the server automatically pushes the updated data via WebSocket. All useQuery(api.tasks.list) hooks refresh instantly — no manual refetching needed!

7. Add Seed Data (Optional)

Create gencow/seed.ts to populate the database with test data:

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 (Pro+ only), use gencow deploy --prod.

Next Steps