# Gencow Documentation — Complete Reference > This file contains the full documentation for the Gencow framework. > Use this as a single-file reference for AI-assisted development. --- ## Introduction > Gencow — The fullstack framework for vibe coders Gencow is a **fullstack backend framework** for vibe coders who want to ship fast without compromising on quality. Build complete backend APIs with TypeScript — auth, database, realtime, file storage, AI, and cloud deployment — all from a single `gencow/` folder. Think of it as **Convex + Supabase**, but simpler: - **Schema-first** — Define your database with TypeScript (Drizzle ORM) - **Instant APIs** — Write queries & mutations, get type-safe endpoints automatically - **Built-in Auth** — User management powered by better-auth (session-based, token-based) - **Realtime** — WebSocket push model — `useQuery` auto-refreshes on data changes - **File Storage** — `ctx.storage.store()` / `getUrl()` — Convex-compatible pattern - **AI-Ready** — Add AI, RAG, Memory, Tools with `gencow add AI` - **Cron Jobs** — Scheduled tasks with `cronJobs()` — interval, daily, weekly, cron - **Cloud Deploy** — `gencow deploy` and you're live on `https://.gencow.app` ### Why Gencow? | Feature | Gencow | Convex | Supabase | Firebase | |---|---|---|---|---| | Schema | Drizzle (TypeScript) | Convex schema | SQL (Console) | NoSQL (Console) | | Query/Mutation | `defineQuery` / `defineMutation` | `query` / `mutation` | SQL / REST | Firestore API | | Auth | Built-in (better-auth) | Clerk/Auth0 | Built-in | Built-in | | Realtime | WebSocket push | Reactive queries | Realtime extension | Firestore listeners | | File Storage | `ctx.storage` | `ctx.storage` | Storage | Cloud Storage | | AI Components | `gencow add AI` | Manual | Manual | Vertex AI | | Cron Jobs | `cronJobs()` | `crons` | pg_cron | Cloud Functions | | Deploy | `gencow deploy` | `npx convex deploy` | Dashboard | `firebase deploy` | | Self-host | ✅ (Docker) | ❌ | ✅ | ❌ | | Local Dev DB | PGlite (zero setup) | SQLite | PostgreSQL | Emulators | ### Who is this for? - **Vibe coders** who use AI assistants (Cursor, Copilot, Claude) to build apps - **Solo developers** who want a full backend in minutes - **Indie hackers** who need auth + DB + AI + file storage without setup overhead - **Agencies** building prototypes and MVPs for clients ### Tech Stack | Layer | Technology | |---|---| | **Runtime** | Bun | | **HTTP Server** | Hono | | **Database** | PGlite (local) / PostgreSQL (production) | | **ORM** | Drizzle ORM | | **Auth** | better-auth (session-based + JWT) | | **Validation** | Zod + built-in `v` validator | | **Realtime** | Native WebSocket (push model) | | **AI** | Vercel AI SDK (OpenAI GPT-4o/4o-mini) | | **Cloud** | Gencow Platform (gencow.app) | ### Architecture ``` Your App ├── gencow/ ← Backend code (this is what you write) │ ├── schema.ts ← Database tables (Drizzle ORM) │ ├── auth-schema.ts ← Auth tables (auto-generated) │ ├── auth.ts ← Auth config (defineAuth) │ ├── tasks.ts ← Your queries & mutations │ ├── files.ts ← File upload/download logic │ ├── crons.ts ← Scheduled jobs │ ├── seed.ts ← Seed data (optional) │ ├── index.ts ← Re-exports all modules │ ├── api.ts ← Auto-generated type-safe API client │ └── README.md ← Auto-generated AI guide ├── src/ ← Frontend (React, Next.js, Vite, etc.) ├── gencow.config.ts ← Project configuration ├── gencow.json ← Cloud app ID (auto-created on deploy) ├── drizzle.config.ts ← Database config (PGlite/PostgreSQL) └── package.json ``` **How it works:** 1. You define database tables in `gencow/schema.ts` using Drizzle ORM 2. You write queries and mutations in `gencow/*.ts` using `defineQuery` / `defineMutation` 3. `gencow dev` starts a Hono server with hot-reload, auto-generates `api.ts` 4. Your React frontend uses `useQuery(api.tasks.list)` — data is reactive via WebSocket 5. `gencow deploy` ships everything to Gencow Cloud (PostgreSQL + isolated container) ### Next Steps - [Installation](/docs/getting-started/installation) — Create your first project - [Quickstart](/docs/getting-started/quickstart) — Build a Todo app in 5 minutes - [Project Structure](/docs/getting-started/project-structure) — Understand every file --- ## Installation > Install Gencow and create your first project ### Prerequisites - **Bun** (recommended) or **Node.js 18+** - **npm**, **pnpm**, or **bun** package manager ### Create a New Project ```bash ## Create in a new directory npx gencow init my-app cd my-app ## Or initialize in current directory npx gencow init . --force ``` #### Flags | Flag | Description | |---|---| | `--template ` or `-t` | Select a template (see below) | | `--force` or `-f` | Initialize in a non-empty directory (preserves existing files, merges `package.json`) | #### What Gets Created ``` my-app/ ├── gencow/ │ ├── schema.ts ← Your database tables │ ├── auth-schema.ts ← Auth tables (better-auth) │ ├── auth.ts ← Auth configuration │ ├── crons.ts ← Cron job scheduler (template) │ ├── index.ts ← Module re-exports │ ├── SECURITY.md ← Security checklist for AI coders │ └── (template files) ← Depends on chosen template ├── gencow.config.ts ← Project configuration ├── drizzle.config.ts ← Database config ├── tsconfig.json ├── package.json ├── .env ← Environment variables (local only) └── .gitignore ``` #### Auto-Installed Dependencies | Package | Purpose | |---|---| | `gencow` | CLI + bundled server runtime | | `@gencow/core` | `defineQuery`, `defineMutation`, `cronJobs`, `v` validator | | `drizzle-orm` | Type-safe SQL ORM | | `drizzle-kit` | Schema migration tool | | `better-auth` | Authentication framework | | `@electric-sql/pglite` | Embedded PostgreSQL for local dev (zero setup) | | `postgres` | PostgreSQL client for production | ### Available Templates ```bash ## Interactive selection (default) npx gencow init my-app ## Or specify directly npx gencow init my-app --template fullstack ``` | # | Template | Description | Includes | |---|---|---|---| | 1 | `default` | Empty project | Basic schema + index.ts | | 2 | `task-app` | Task + Files CRUD backend | tasks.ts, files.ts with full CRUD | | 3 | `fullstack` | Tasks + Files + AI Chat + Agent | All of task-app + ai.ts + prompt.md | | 4 | `ai-chat` | AI Chatbot + Memory backend | chat.ts, ai.ts + prompt.md | #### What `prompt.md` Contains Templates `fullstack` and `ai-chat` include a `prompt.md` file — a **vibe-coding prompt** designed to be given to AI assistants (Cursor, Copilot, etc.). It includes: - All available API endpoints with types - Authentication setup instructions - `useQuery` / `useMutation` usage patterns - Security rules for backend code - Recommended UI structure ### Start Development ```bash gencow dev ``` This starts: - 🔧 **Hono server** at `http://localhost:5456` with hot-reload - 📊 **Admin Dashboard** at `http://localhost:5456/_admin` for data browsing - 🗄️ **PGlite** embedded PostgreSQL (zero setup, zero config) - 📝 **Auto-codegen** — `gencow/api.ts` and `gencow/README.md` regenerate on file changes #### Dev Modes ```bash ## Default: Cloud development (connects to Gencow Cloud DB) gencow dev ## Local-only: Uses PGlite embedded database gencow dev --local ## Verbose: Show all HTTP logs (including admin/ws) gencow dev --verbose ``` ### Using with Existing Projects If you already have a Next.js, Vite, or other frontend project: ```bash ## Navigate to your project root cd my-existing-project ## Initialize Gencow in the current directory npx gencow init . --force ## Install dependencies bun install # or npm install ## Start Gencow backend gencow dev ``` The `--force` flag: - Preserves all existing files - Merges Gencow dependencies into your existing `package.json` - Creates `gencow/` folder alongside your existing `src/` - Skips `.env` and `tsconfig.json` if they already exist ### Next Steps - [Quickstart](/docs/getting-started/quickstart) — Build a Todo app in 5 minutes - [Project Structure](/docs/getting-started/project-structure) — Understand every file - [Schema Guide](/docs/guides/schema) — Learn about schema patterns --- ## Quickstart > Build a fullstack 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 ```bash 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: ```typescript import { pgTable, serial, text, boolean, timestamp } from "drizzle-orm/pg-core"; import { user } from "./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(), }); ``` > **Security:** The `userId` column with `.references()` ensures every task belongs to a user. This is critical for data isolation — Gencow enforces this pattern. ### 3. Understand Queries & Mutations Open `gencow/tasks.ts` — the template provides full CRUD: ```typescript import { query, mutation, v } from "@gencow/core"; import { eq, and, desc } from "drizzle-orm"; import { tasks } from "./schema"; // List all tasks for the current user (newest first) export const list = query("tasks.list", { args: {}, handler: async (ctx) => { const session = ctx.auth.requireAuth(); return ctx.db .select() .from(tasks) .where(eq(tasks.userId, session.user.id)) .orderBy(desc(tasks.createdAt)); }, }); // Create a new task export const create = mutation("tasks.create", { args: { title: v.string() }, handler: async (ctx, { title }) => { const session = ctx.auth.requireAuth(); const [task] = await ctx.db .insert(tasks) .values({ title, userId: session.user.id }) .returning(); return task; }, }); // Toggle done status export const toggle = mutation("tasks.toggle", { args: { id: v.number() }, handler: async (ctx, { id }) => { const session = ctx.auth.requireAuth(); const [task] = await ctx.db .select() .from(tasks) .where(and(eq(tasks.id, id), eq(tasks.userId, session.user.id))); if (!task) throw new Error("Task not found"); await ctx.db .update(tasks) .set({ done: !task.done }) .where(eq(tasks.id, id)); }, }); // Delete a task (with ownership check) export const _delete = mutation("tasks.delete", { args: { id: v.number() }, handler: async (ctx, { id }) => { const session = ctx.auth.requireAuth(); await ctx.db .delete(tasks) .where(and(eq(tasks.id, id), eq(tasks.userId, session.user.id))); }, }); ``` ### 4. Register Modules Open `gencow/index.ts` — re-export all modules: ```typescript export * as tasks from "./tasks"; // export * as files from "./files"; // uncomment when needed ``` > **Important pattern:** Always use `export * as moduleName` — don't add suffixes like `Module` or `Mod`. ### 5. Start the Dev Server ```bash gencow dev ``` You'll see: ``` Gencow Dev (local) ▸ Functions: ./gencow ▸ Storage: ./.gencow/uploads ▸ DB: ./.gencow/data ✓ 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 ``` #### 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 React Frontend Install the React SDK: ```bash npm install @gencow/react ``` #### Set Up Auth ```typescript // src/lib/auth.ts import { gencowAuth } from "@gencow/react"; export const { signIn, signUp, signOut, useAuth } = gencowAuth(); ``` #### Set Up the Provider ```tsx // src/main.tsx (or layout.tsx) import { GencowProvider } from "@gencow/react"; import { useAuth } from "./lib/auth"; function AppWrapper({ children }) { const { token } = useAuth(); const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456"; return ( {children} ); } ``` #### Build the Todo UI ```tsx import { useQuery, useMutation } from "@gencow/react"; import { api } from "../gencow/api"; // auto-generated function TodoApp() { const tasks = useQuery(api.tasks.list); const [createTask, isCreating] = useMutation(api.tasks.create); const [toggleTask] = useMutation(api.tasks.toggle); const [deleteTask] = useMutation(api.tasks.delete); if (tasks === undefined) return
Loading...
; return (

My Todos

{/* Create form */}
{ e.preventDefault(); const title = new FormData(e.currentTarget).get("title") as string; createTask({ title }); e.currentTarget.reset(); }}>
{/* Task list */}
    {tasks.map((t) => (
  • toggleTask({ id: t.id })} style={{ cursor: "pointer" }} > {t.done ? "✅" : "⬜"} {t.title}
  • ))}
); } ``` > **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: ```typescript 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" }, ]); } ``` ```bash ## Run seed (server must be running) gencow db:seed ``` ### 8. Deploy ```bash ## Login (first time only) gencow login ## Deploy backend gencow deploy ## Deploy frontend (if you have one) VITE_API_URL=https://.gencow.app npm run build gencow deploy --static dist/ ``` That's it! Your app is live on `https://.gencow.app`. 🎉 ### Next Steps - [Project Structure](/docs/getting-started/project-structure) — Understand every config file - [Schema Guide](/docs/guides/schema) — Advanced schema patterns - [Authentication](/docs/guides/authentication) — Auth setup in detail - [AI Engine](/docs/ai/ai-engine) — Add AI to your app - [Deployment](/docs/guides/deployment) — Cloud deployment details --- ## Project Structure > Understand every file and folder in a Gencow project ### Overview A Gencow project follows a simple convention: your backend lives in `gencow/`, and everything else (frontend, configs) lives alongside it. ``` my-app/ ├── gencow/ ← Backend code │ ├── schema.ts ← Database tables (Drizzle ORM) │ ├── auth-schema.ts ← Auth tables (auto-generated by init) │ ├── auth.ts ← Auth configuration (defineAuth) │ ├── index.ts ← Re-exports all modules │ ├── tasks.ts ← Your queries & mutations │ ├── crons.ts ← Scheduled jobs │ ├── seed.ts ← Seed data (optional) │ ├── api.ts ← ⚡ Auto-generated (do not edit) │ ├── README.md ← ⚡ Auto-generated AI guide │ └── SECURITY.md ← Security checklist ├── .gencow/ ← Runtime data (gitignored) │ ├── data/ ← PGlite database files │ ├── uploads/ ← File storage │ ├── backups/ ← Auto-backups before dev restart │ ├── server.js ← Bundled server copy │ └── dashboard/ ← Admin dashboard assets ├── migrations/ ← SQL migration files ├── gencow.config.ts ← Project configuration ├── gencow.json ← Cloud app ID (created on deploy) ├── drizzle.config.ts ← Database config ├── package.json ├── tsconfig.json ├── .env ← Environment variables (local only) └── .gitignore ``` ### Configuration Files #### `gencow.config.ts` The main project configuration file: ```typescript export default { // Path to your backend functions directory functionsDir: "./gencow", // Path to your Drizzle schema file schema: "./gencow/schema.ts", // File storage directory storage: "./.gencow/uploads", // Local database configuration db: { url: "./.gencow/data" }, // API server port port: 5456, }; ``` | Option | Default | Description | |---|---|---| | `functionsDir` | `"./gencow"` | Directory containing your backend code | | `schema` | `"./gencow/schema.ts"` | Path to Drizzle schema file | | `storage` | `"./.gencow/uploads"` | File storage directory | | `db.url` | `"./.gencow/data"` | PGlite data directory (local) | | `port` | `5456` | Dev server port | #### `gencow.json` Auto-created when you run `gencow deploy`. Contains your cloud app identity: ```json { "appId": "null-mint-9625", "displayName": "my-app", "platformUrl": "https://gencow.app" } ``` > **Do not edit `appId`** — it's a unique identifier assigned by the platform. Changing it will create a new app on next deploy. #### `drizzle.config.ts` Database configuration for Drizzle Kit (migrations): ```typescript import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", schema: ["./gencow/schema.ts", "./gencow/auth-schema.ts"], out: "./migrations", // Auto-detects: DATABASE_URL → PostgreSQL, otherwise → PGlite ...(process.env.DATABASE_URL ? { dbCredentials: { url: process.env.DATABASE_URL } } : { driver: "pglite", dbCredentials: { url: "./.gencow/data" } }), }); ``` ### The `gencow/` Folder #### `schema.ts` — Database Tables Define your tables using Drizzle ORM's `pgTable`: ```typescript import { pgTable, serial, text, boolean, timestamp } from "drizzle-orm/pg-core"; import { user } from "./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(), }); ``` #### `auth-schema.ts` — Auth Tables Auto-generated by `gencow init`. Contains `user`, `session`, and `account` tables required by better-auth. **Do not remove or modify** unless you know what you're doing. #### `auth.ts` — Auth Configuration Customize authentication behavior: ```typescript import { defineAuth } from "@gencow/core"; export default defineAuth({ emailAndPassword: { enabled: true }, // emailVerification: { sendVerificationEmail: async ({ email, url }) => { ... } }, }); ``` #### `index.ts` — Module Re-exports **Critical file** — tells the server which modules to register: ```typescript export * as tasks from "./tasks"; export * as files from "./files"; // export * as chat from "./chat"; ``` > **Pattern:** Always use `export * as moduleName` (not `export * as tasksModule` or `export * as TasksMod`). #### `crons.ts` — Cron Jobs Define scheduled tasks: ```typescript import { cronJobs } from "@gencow/core"; const crons = cronJobs(); crons.interval("syncData", { minutes: 30 }, "data.sync"); crons.daily("report", { hour: 9 }, "reports.generate"); export default crons; // ← Required! ``` #### `seed.ts` — Seed Data (Optional) Populate the database with test data: ```typescript import { tasks } from "./schema"; export default async function seed(ctx) { await ctx.db.insert(tasks).values([ { title: "First task", userId: "test-user" }, ]); } ``` Run with `gencow db:seed`. #### `api.ts` — Auto-Generated (⚡ Do Not Edit) Generated by `gencow dev` on every file change. Provides type-safe API references for your React frontend: ```typescript // Generated by Gencow — do not edit manually. import { defineQuery, defineMutation } from "@gencow/react"; import type * as tasks from "./tasks.ts"; export const api = { tasks: { list: defineQuery("tasks.list"), create: defineMutation("tasks.create"), toggle: defineMutation("tasks.toggle"), delete: defineMutation("tasks.delete"), }, } as const; ``` #### `README.md` — Auto-Generated AI Guide (⚡ Do Not Edit) Generated alongside `api.ts`. Contains a comprehensive vibe-coding guide that you can paste into AI assistants. Includes API reference, React hook usage, auth setup, cron patterns, and deployment commands. ### The `.gencow/` Folder (Gitignored) | Path | Purpose | |---|---| | `.gencow/data/` | PGlite database files (local dev) | | `.gencow/uploads/` | Uploaded files (via `ctx.storage.store()`) | | `.gencow/backups/` | Auto-backups (kept last 3, created on `gencow dev`) | | `.gencow/server.js` | Bundled server copy (for standalone projects) | | `.gencow/dashboard/` | Admin dashboard static assets | ### Environment Variables #### `.env` (Local Development Only) ```bash ## Gencow auto-populates this after deploy: VITE_API_URL=https://my-app-id.gencow.app ## Your secrets (local only): OPENAI_API_KEY=sk-your-key-here DATABASE_URL=postgres://user:pass@host/db ``` #### Production Environment Manage via CLI: ```bash gencow env set OPENAI_API_KEY=sk-... gencow env set DATABASE_URL=postgres://... gencow env list gencow env push # Push all .env vars to cloud ``` > **Security:** `.env` is gitignored. Production environment variables are encrypted at rest. Never expose secrets in frontend code. ### Next Steps - [Schema Guide](/docs/guides/schema) — Advanced table patterns - [Queries & Mutations](/docs/guides/queries) — Read and write data - [Authentication](/docs/guides/authentication) — Auth setup in detail --- ## Schema > Define your database schema with Drizzle ORM Gencow uses **Drizzle ORM** for schema definition. Your schema lives in `gencow/schema.ts` and uses PostgreSQL column types. ### Defining Tables ```typescript import { pgTable, serial, text, boolean, timestamp, integer, jsonb, uuid } from "drizzle-orm/pg-core"; import { user } from "./auth-schema"; export const posts = pgTable("posts", { id: serial("id").primaryKey(), title: text("title").notNull(), content: text("content"), published: boolean("published").default(false).notNull(), views: integer("views").default(0).notNull(), metadata: jsonb("metadata"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); ``` ### Column Types | Type | Drizzle | PostgreSQL | Example | |---|---|---|---| | Auto-increment | `serial("id")` | `SERIAL` | Primary keys | | Text | `text("name")` | `TEXT` | Strings of any length | | Integer | `integer("count")` | `INTEGER` | Numbers, counts | | Boolean | `boolean("active")` | `BOOLEAN` | Flags, toggles | | Timestamp | `timestamp("created_at")` | `TIMESTAMP` | Dates, times | | JSON | `jsonb("metadata")` | `JSONB` | Structured data, settings | | UUID | `uuid("id")` | `UUID` | Unique identifiers | | Enum | `text("status")` | `TEXT` | Use text with validation | | Real | `real("score")` | `REAL` | Floating point | | Decimal | `numeric("price", { precision: 10, scale: 2 })` | `NUMERIC` | Money, precise values | ### Column Modifiers ```typescript // Required field text("title").notNull() // Default value boolean("done").default(false).notNull() // Auto-timestamp timestamp("created_at").defaultNow().notNull() // Primary key serial("id").primaryKey() // UUID primary key (alternative) uuid("id").defaultRandom().primaryKey() // Unique constraint text("email").notNull().unique() ``` ### Relationships #### Foreign Keys (User Ownership) **Always** use `userId` with `.references()` for data that belongs to a user: ```typescript userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), ``` The `onDelete: "cascade"` ensures that when a user is deleted, all their data is automatically cleaned up. #### One-to-Many A post has many comments: ```typescript export const comments = pgTable("comments", { id: serial("id").primaryKey(), content: text("content").notNull(), postId: integer("post_id") .notNull() .references(() => posts.id, { onDelete: "cascade" }), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), }); ``` #### Many-to-Many (Join Table) Users can belong to multiple teams: ```typescript export const teams = pgTable("teams", { id: serial("id").primaryKey(), name: text("name").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }); export const teamMembers = pgTable("team_members", { id: serial("id").primaryKey(), teamId: integer("team_id") .notNull() .references(() => teams.id, { onDelete: "cascade" }), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), role: text("role").default("member").notNull(), // "admin" | "member" joinedAt: timestamp("joined_at").defaultNow().notNull(), }); ``` #### Self-Referencing Categories with parent-child hierarchy: ```typescript export const categories = pgTable("categories", { id: serial("id").primaryKey(), name: text("name").notNull(), parentId: integer("parent_id") .references(() => categories.id, { onDelete: "set null" }), }); ``` ### JSONB for Flexible Data Use `jsonb` for settings, metadata, or any semi-structured data: ```typescript export const userSettings = pgTable("user_settings", { id: serial("id").primaryKey(), userId: text("user_id") .notNull() .unique() .references(() => user.id, { onDelete: "cascade" }), preferences: jsonb("preferences").$type<{ theme: "light" | "dark"; language: string; notifications: boolean; }>().default({ theme: "light", language: "en", notifications: true }), }); ``` ### Applying Schema Changes #### Quick Sync (Development) Push changes instantly without migration files: ```bash gencow db:push ``` This applies changes **instantly** — like Convex's automatic schema sync. Best for rapid prototyping. #### Migration Files (Production) For production deployments with controlled migrations: ```bash ## 1. Generate SQL migration files from schema changes gencow db:generate ## 2. Apply pending migrations gencow db:migrate ``` Migration files are saved in `migrations/` and are version-controlled. #### Comparison | | `db:push` | `db:generate` + `db:migrate` | |---|---|---| | Speed | Instant | Two-step | | Migration files | No | Yes (SQL files in `migrations/`) | | Rollback | No | Manual via SQL | | Best for | Development | Production | | Auto-detects | Cloud/Local | Cloud/Local | > **Note:** `gencow dev` automatically runs `db:generate` on startup, so migration files are always up to date when developing. ### Database Commands Reference | Command | Description | |---|---| | `gencow db:push` | Sync schema → DB instantly (no migration files) | | `gencow db:generate` | Generate SQL migration files | | `gencow db:migrate` | Apply pending migrations | | `gencow db:seed` | Run `gencow/seed.ts` (insert test data) | | `gencow db:reset` | TRUNCATE all data + re-run seed.ts | | `gencow db:restore` | Restore from backup | | `gencow db:studio` | Open Drizzle Studio (visual DB browser) | ### Next Steps - [Queries](/docs/guides/queries) — Read data with `defineQuery` - [Mutations](/docs/guides/mutations) — Write data with `defineMutation` --- ## Queries > Read data with type-safe queries Queries are **read-only** functions that fetch data from the database. They're defined with `query()` from `@gencow/core`. ### Defining Queries ```typescript import { query, v } from "@gencow/core"; import { eq, desc } from "drizzle-orm"; import { posts } from "./schema"; export const list = query("posts.list", { args: {}, handler: async (ctx) => { const session = ctx.auth.requireAuth(); return ctx.db .select() .from(posts) .where(eq(posts.userId, session.user.id)) .orderBy(desc(posts.createdAt)); }, }); ``` #### Key Parts | Part | Description | |---|---| | `"posts.list"` | Unique name — must match `{module}.{export}` pattern | | `args` | Input validation schema (using `v` validator or Zod) | | `handler(ctx, args)` | Async function that reads from `ctx.db` | | `ctx.auth.requireAuth()` | Enforces authentication, returns session | | `ctx.db` | Drizzle ORM instance for database queries | ### Query with Arguments ```typescript export const getById = query("posts.getById", { args: { id: v.number() }, handler: async (ctx, { id }) => { const session = ctx.auth.requireAuth(); const [post] = await ctx.db .select() .from(posts) .where(eq(posts.id, id)); return post || null; }, }); ``` ### Filtering & Sorting ```typescript import { eq, and, or, like, between, desc, asc, gt, lt, isNull, isNotNull } from "drizzle-orm"; // Multiple conditions export const search = query("posts.search", { args: { keyword: v.string(), published: v.optional(v.boolean()) }, handler: async (ctx, { keyword, published }) => { const session = ctx.auth.requireAuth(); const conditions = [eq(posts.userId, session.user.id)]; if (keyword) { conditions.push(like(posts.title, `%${keyword}%`)); } if (published !== undefined) { conditions.push(eq(posts.published, published)); } return ctx.db .select() .from(posts) .where(and(...conditions)) .orderBy(desc(posts.createdAt)); }, }); ``` #### Available Operators | Operator | Example | Description | |---|---|---| | `eq` | `eq(posts.id, 1)` | Equals | | `and` | `and(eq(...), eq(...))` | Both conditions | | `or` | `or(eq(...), eq(...))` | Either condition | | `like` | `like(posts.title, "%search%")` | Pattern match | | `gt` / `lt` | `gt(posts.views, 100)` | Greater / less than | | `between` | `between(posts.views, 10, 100)` | Range | | `isNull` | `isNull(posts.content)` | Is null | | `isNotNull` | `isNotNull(posts.content)` | Is not null | | `desc` / `asc` | `desc(posts.createdAt)` | Sorting | ### Pagination #### Offset-Based ```typescript export const listPaged = query("posts.listPaged", { args: { page: v.optional(v.number()), limit: v.optional(v.number()) }, handler: async (ctx, { page = 1, limit = 20 }) => { const session = ctx.auth.requireAuth(); const offset = (page - 1) * limit; const items = await ctx.db .select() .from(posts) .where(eq(posts.userId, session.user.id)) .orderBy(desc(posts.createdAt)) .limit(limit) .offset(offset); return { items, page, limit }; }, }); ``` #### Cursor-Based ```typescript export const listCursor = query("posts.listCursor", { args: { cursor: v.optional(v.number()), limit: v.optional(v.number()) }, handler: async (ctx, { cursor, limit = 20 }) => { const session = ctx.auth.requireAuth(); const conditions = [eq(posts.userId, session.user.id)]; if (cursor) { conditions.push(lt(posts.id, cursor)); } const items = await ctx.db .select() .from(posts) .where(and(...conditions)) .orderBy(desc(posts.id)) .limit(limit + 1); const hasMore = items.length > limit; const data = hasMore ? items.slice(0, -1) : items; const nextCursor = hasMore ? data[data.length - 1].id : null; return { items: data, nextCursor }; }, }); ``` ### Public Queries (No Auth Required) For endpoints that don't require authentication: ```typescript export const listPublic = query("posts.listPublic", { args: {}, handler: async (ctx) => { // No requireAuth() — anyone can access return ctx.db .select() .from(posts) .where(eq(posts.published, true)) .orderBy(desc(posts.createdAt)) .limit(50); }, }); ``` > **Warning:** Public queries should only expose non-sensitive data. Never return user private data without authentication. ### Joining Tables ```typescript import { posts, comments } from "./schema"; export const getWithComments = query("posts.getWithComments", { args: { id: v.number() }, handler: async (ctx, { id }) => { const session = ctx.auth.requireAuth(); const post = await ctx.db .select() .from(posts) .where(and(eq(posts.id, id), eq(posts.userId, session.user.id))) .limit(1); if (!post[0]) return null; const postComments = await ctx.db .select() .from(comments) .where(eq(comments.postId, id)) .orderBy(desc(comments.createdAt)); return { ...post[0], comments: postComments }; }, }); ``` ### Using from React ```tsx import { useQuery } from "@gencow/react"; import { api } from "../gencow/api"; function PostList() { // Basic query — returns Post[] | undefined const posts = useQuery(api.posts.list); // With arguments const post = useQuery(api.posts.getById, { id: 42 }); // Conditional query — skip when no ID selected const detail = useQuery( api.posts.getById, selectedId ? { id: selectedId } : "skip" ); // Public query (no auth needed) const publicPosts = useQuery(api.posts.listPublic, {}, { public: true }); if (posts === undefined) return
Loading...
; return (
    {posts.map((p) => (
  • {p.title}
  • ))}
); } ``` > **Realtime:** `useQuery` automatically subscribes to WebSocket updates. When data changes on the server, your component re-renders with fresh data — no manual refetching. ### Argument Validation Use the `v` validator: ```typescript args: { id: v.number(), title: v.string(), count: v.optional(v.number()), // optional tags: v.array(v.string()), // string array settings: v.object({ // nested object theme: v.string(), count: v.number(), }), } ``` | Validator | Description | |---|---| | `v.string()` | String | | `v.number()` | Number | | `v.boolean()` | Boolean | | `v.optional(v.X())` | Optional field | | `v.array(v.X())` | Array of type | | `v.object({...})` | Nested object | ### Security Rules > **Always filter by userId** in queries that return user data. This prevents data leakage between users. ```typescript // ✅ Correct — filter by userId ctx.db.select().from(posts).where(eq(posts.userId, session.user.id)); // ❌ Wrong — returns ALL users' data ctx.db.select().from(posts); ``` ### Next Steps - [Mutations](/docs/guides/mutations) — Write data with `defineMutation` - [Authentication](/docs/guides/authentication) — Auth setup details - [Realtime](/docs/guides/realtime) — How WebSocket sync works --- ## Mutations > Write data with type-safe mutations Mutations are functions that **create, update, or delete** data. They're defined with `mutation()` from `@gencow/core`. ### Defining Mutations ```typescript import { mutation, v } from "@gencow/core"; import { eq, and } from "drizzle-orm"; import { posts } from "./schema"; export const create = mutation("posts.create", { args: { title: v.string(), content: v.optional(v.string()), }, handler: async (ctx, { title, content }) => { const session = ctx.auth.requireAuth(); const [post] = await ctx.db .insert(posts) .values({ title, content, userId: session.user.id }) .returning(); return post; }, }); ``` ### CRUD Patterns #### Create ```typescript export const create = mutation("posts.create", { args: { title: v.string(), content: v.optional(v.string()) }, handler: async (ctx, { title, content }) => { const session = ctx.auth.requireAuth(); const [post] = await ctx.db .insert(posts) .values({ title, content, userId: session.user.id }) .returning(); return post; }, }); ``` #### Update (with Ownership Check) ```typescript export const update = mutation("posts.update", { args: { id: v.number(), title: v.optional(v.string()), content: v.optional(v.string()), published: v.optional(v.boolean()), }, handler: async (ctx, { id, ...updates }) => { const session = ctx.auth.requireAuth(); // Remove undefined values const data = Object.fromEntries( Object.entries(updates).filter(([_, v]) => v !== undefined) ); const [updated] = await ctx.db .update(posts) .set({ ...data, updatedAt: new Date() }) .where(and(eq(posts.id, id), eq(posts.userId, session.user.id))) .returning(); if (!updated) throw new Error("Post not found"); return updated; }, }); ``` #### Delete (with Ownership Check) ```typescript // Use _delete (underscore prefix) because 'delete' is a reserved word export const _delete = mutation("posts.delete", { args: { id: v.number() }, handler: async (ctx, { id }) => { const session = ctx.auth.requireAuth(); // Always verify ownership before deletion await ctx.db .delete(posts) .where(and( eq(posts.id, id), eq(posts.userId, session.user.id) )); }, }); ``` > **Important:** Export name `_delete` maps to API name `posts.delete`. The underscore prefix is automatically handled. ### File Upload with FormData Mutations can accept `FormData` for file uploads: ```typescript export const upload = mutation("files.upload", { args: {}, // args come from FormData handler: async (ctx) => { const session = ctx.auth.requireAuth(); const file = ctx.file; // File from FormData if (!file) throw new Error("No file provided"); // Store file using ctx.storage const storageId = await ctx.storage.store(file); const url = ctx.storage.getUrl(storageId); return { storageId, url, name: file.name, size: file.size }; }, }); ``` #### React Upload Example ```tsx const [upload, isPending] = useMutation(api.files.upload); const handleUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const formData = new FormData(); formData.append("file", file); await upload(formData); }; ``` ### Scheduling Long-Running Tasks Mutations have a **10-second timeout**. For tasks that take longer (AI calls, external APIs, crawling), split them into steps: ```typescript export const startProcess = mutation("pipeline.start", { args: { url: v.string() }, handler: async (ctx, { url }) => { const session = ctx.auth.requireAuth(); // Step 1: Quick validation (< 10 sec) const isValid = await validateUrl(url); if (!isValid) throw new Error("Invalid URL"); // Schedule the heavy work as a separate step await ctx.scheduler.runAfter(0, "pipeline.processStep", { url, userId: session.user.id, }); return { status: "started" }; }, }); export const processStep = mutation("pipeline.processStep", { args: { url: v.string(), userId: v.string() }, handler: async (ctx, { url, userId }) => { // This runs independently — can take up to 10 sec const result = await crawlAndExtract(url); // Save results await ctx.db.insert(results).values({ data: result, userId, }); }, }); ``` > **Pattern:** Break long pipelines into short mutations connected by `ctx.scheduler.runAfter()`. ### Error Handling ```typescript export const create = mutation("posts.create", { args: { title: v.string() }, handler: async (ctx, { title }) => { const session = ctx.auth.requireAuth(); // Validation errors if (title.length > 200) { throw new Error("Title too long (max 200 characters)"); } // Database operations may fail try { const [post] = await ctx.db .insert(posts) .values({ title, userId: session.user.id }) .returning(); return post; } catch (e) { if (e.message.includes("unique")) { throw new Error("A post with this title already exists"); } throw e; } }, }); ``` #### React Error Handling ```tsx const [create, isPending, error] = useMutation(api.posts.create); // error is Error | null {error &&
{error.message}
} ``` ### Calling Other Functions Within Mutations When you need to call another module's function within a mutation, **import it directly** — don't use fetch: ```typescript import { fetchNews } from "./naverApi"; export const processNews = mutation("pipeline.processNews", { args: { keyword: v.string() }, handler: async (ctx, { keyword }) => { // ✅ Direct import — fast, no network overhead const result = await fetchNews.handler(ctx, { keyword }); // ❌ Don't self-fetch // const res = await fetch("/api/mutation", { ... }); return result; }, }); ``` ### Using from React ```tsx import { useMutation } from "@gencow/react"; import { api } from "../gencow/api"; function CreatePost() { const [createPost, isPending, error] = useMutation(api.posts.create); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const data = new FormData(e.currentTarget as HTMLFormElement); await createPost({ title: data.get("title") as string, content: data.get("content") as string, }); }; return (