Project Structure

Understand the frontend, gencow backend, config, and deploy files in a Gencow project

Overview

A Gencow project follows a simple convention: your backend lives in gencow/, and everything else (the recommended Vite + React frontend, configs) lives alongside it. Local env files are split by responsibility: keep frontend build-time vars in the project-root .env, and keep backend secrets/runtime vars in gencow/.env by default (or a custom envFile path from gencow.config.js).

my-app/
├── gencow/                  ← Backend code
│   ├── .env                 ← Backend env (local secrets/runtime vars)
│   ├── schema.ts            ← Database tables (Drizzle ORM)
│   ├── schema-auth.ts       ← Auth table boundary (re-export or fork)
│   ├── generated/           ← Server codegen (do not edit by hand)
│   │   ├── schema-auth.gen.ts ← Generated Better Auth tables (codegen)
│   │   └── db-schema.gen.ts ← Aggregated schema for typed ctx.db (codegen)
│   ├── migrations/          ← Versioned Drizzle migration files
│   ├── auth.ts              ← Auth configuration (defineAuth)
│   ├── runtime.ts           ← Typed `procedure`, `httpRoute`, and `createCrud` bound to your schema + auth
│   ├── index.ts             ← Registers procedures, routes, and cron schedules (`defineApi`)
│   ├── tasks.ts             ← Your queries & mutations
│   ├── seed.ts              ← Seed data (optional)
│   ├── README.md            ← ⚡ Auto-generated AI guide
│   └── SECURITY.md          ← Security checklist
├── .gencow/                 ← Runtime data (gitignored)
│   ├── uploads/             ← File storage
│   ├── server.js            ← Bundled server copy
│   └── dashboard/           ← Admin dashboard assets
├── src/                     ← Vite + React frontend (recommended)
│   ├── main.tsx             ← React entry point
│   ├── App.tsx              ← App shell
│   └── gencow/              ← Generated frontend API client
├── gencow.config.js         ← Project configuration
├── gencow.json              ← Cloud app ID (created on deploy)
├── drizzle.config.ts        ← Database config
├── package.json
├── tsconfig.json
├── .env                     ← Frontend env (local build-time/public vars)
└── .gitignore

Configuration Files

gencow.config.js

The main project configuration file:

/** @type {import('@gencow/core').GencowConfig} */
export default {
    /** Backend module root (`index.ts`, `schema.ts`, …). */
    rootDir: "./gencow",

    /** Drizzle schema entry files. */
    schema: ["./gencow/schema-auth.ts", "./gencow/schema.ts"],

    codegen: {
        /** Client codegen: `api.ts`, `db.d.ts`, `operations.d.ts`. */
        clientOutDir: "./src/gencow",

        /**
         * Server codegen: `db-schema.gen.ts`, `schema-auth.gen.ts`.
         * Default: `{rootDir}/generated` (e.g. `./gencow/generated`).
         */
        // serverOutDir: "./gencow/generated",

        /** Experimental. Set `false` if you fully own auth schema files. */
        authSchema: { emitRelations: true },
    },

    /** Local file upload directory. */
    storage: "./.gencow/uploads",

    /** Local dev database path (PGlite). */
    db: { url: "./.gencow/data" },

    /** Dev server port. */
    port: 5456,
};

The JSDoc @type annotation gives editors full autocomplete and inline docs without adding a runtime import. Because the file is plain JavaScript, you can use variables, process.env, or any other dynamic expression. The CLI loads gencow.config.js with a dynamic import(). Projects from gencow init ship export default together with "type": "module" in package.json. In a CommonJS-only package you can still use module.exports = { ... }; Node interop exposes that as the default export. Legacy gencow.config.ts files (using defineConfig({ ... }) from @gencow/core) are still loaded for backward compatibility, but support only literal values; migrate to gencow.config.js when you need dynamic configuration. If both config files exist, CLI 0.1.183+ uses and deploys only gencow.config.js and warns that .ts was ignored.

gencow codegen writes client artifacts to codegen.clientOutDir and server artifacts to codegen.serverOutDir (defaults to {rootDir}/generated). Better Auth tables from gencow/auth.ts are emitted to schema-auth.gen.ts; gencow/schema-auth.ts is your boundary file (re-export by default, fork when you need manual edits).

Option Default Description
rootDir "./gencow" Backend module root containing index.ts, schema.ts, and related modules
functionsDir Deprecated. Alias for rootDir when rootDir is omitted; if both are set, rootDir wins with a warning
schema ["./gencow/schema-auth.ts", "./gencow/schema.ts"] Path to Drizzle schema files
codegen.clientOutDir "./src/gencow" Client codegen: api.ts, db.d.ts, operations.d.ts
codegen.outDir Deprecated. Same as clientOutDir; if both are set, clientOutDir wins with a warning
codegen.serverOutDir {rootDir}/generated Server codegen: db-schema.gen.ts, schema-auth.gen.ts
codegen.authSchema { emitRelations: true } Experimental. Emits {serverOutDir}/schema-auth.gen.ts; set false only if you fully own auth schema files
storage "./.gencow/uploads" File storage directory
db.url "./.gencow/data" Database data directory
port 5456 Dev server port

Hover GencowConfig in gencow.config.js to see supported keys, comments, and examples directly from @gencow/core.

gencow.json

Auto-created when you run gencow dev or gencow deploy. Contains your cloud app identity:

{
  "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):

import { defineConfig } from "drizzle-kit";

export default defineConfig({
    dialect: "postgresql",
    schema: ["./gencow/schema.ts", "./gencow/schema-auth.ts"],
    out: "./migrations",
    // Database connection
    ...(process.env.DATABASE_URL
        ? { dbCredentials: { url: process.env.DATABASE_URL } }
        : {}),
});

Note — which schema files to list: Drizzle walks the import graph from every path in schema. The correct entries depend on how schema.ts and schema-auth.ts are wired — the same rules apply to gencow.config.js schema (and gencow dev / gencow db:generate use that list, not necessarily this file).

  • Default (import layout): schema.ts imports auth from ./schema-auth for FKs and defineRelations, but does not re-export auth tables. List both files in gencow.config (put schema-auth.ts first). For standalone bunx drizzle-kit, "./gencow/schema.ts" alone is enough — auth tables are included transitively via imports.
  • Re-export layout: schema.ts re-exports auth (export { user, … } from "./schema-auth"). List only "./gencow/schema.ts". Listing schema-auth.ts as well registers the same pgTable definitions twice and causes duplicate table errors.
  • Missing auth tables: If schema-auth.ts is omitted from gencow.config schema while schema.ts only imports (no re-export), migrations may try to drop auth tables (user, session, …) and the Better Auth runtime adapter may fail to load.

Do not list gencow/generated/schema-auth.gen.ts or legacy generated/auth-schema.ts — auth should enter the graph only through gencow/schema-auth.ts.

The gencow/ Folder

schema.ts — Database Tables

Define your tables using Drizzle's pgTable with ownerRls() for automatic Row-Level Security:

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

Note: ownerRls() generates PostgreSQL RLS policies for automatic per-user data isolation. Combined with createCrud(tasks), all CRUD operations enforce authentication and ownership — no manual filtering needed.

gencow/schema-auth.ts — Auth Tables

Scaffolded by gencow init as a thin boundary over codegen. By default it only re-exports gencow/generated/schema-auth.gen.tsdo not edit the re-export; run gencow codegen after changing gencow/auth.ts so the generated file stays in sync.

A minimal email/password setup typically includes user, session, account, and verification, but the exact tables and columns are not fixed — enabling OAuth, organizations, two-factor, or other plugins can add more tables and fields.

To customize auth tables manually (for example RLS policies or extra Drizzle-only columns), copy gencow/generated/schema-auth.gen.ts into gencow/schema-auth.ts, edit in place, and remove the re-export line. Codegen never overwrites schema-auth.ts once it exists.

gencow/generated/db-schema.gen.ts — Typed Schema Map

Auto-generated aggregate of your Drizzle schema files (including auth tables). Used by gencow/runtime.ts for typed ctx.db. Do not edit manually.

auth.ts — Auth Configuration

You own this file. Changes here drive schema-auth.gen.ts on the next gencow codegen — extra user columns, OAuth providers, and Better Auth plugins all affect the generated tables.

import { defineAuth } from "@gencow/core";

export default defineAuth({
    user: {
        /** App-owned columns on `user` — types, defaults, and signup `input` flags. */
        additionalFields: {
            role: { type: "text", default: "user" },
            plan: { type: "text", default: "free", input: true },
        },
    },

    socialProviders: {
        google: {
            clientId: process.env.GOOGLE_CLIENT_ID!,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        },
    },

    // oauth: {
    //     callbackURL: "https://your-frontend.com/auth/callback",
    //     allowedCallbackURLs: ["http://localhost:5173/auth/callback"],
    // },

    // emailVerification: {
    //     sendVerificationEmail: async ({ user, url }) => { /* send mail */ },
    // },
});

When your browser frontend is hosted outside the Gencow app domain, declare that exact origin in gencow.config.js so CORS, Better Auth trusted origins, and OAuth callback validation share the same allowlist:

export default {
    frontendOrigins: ["https://your-frontend.com"],
};

Run gencow codegen (or gencow dev) after editing auth settings so generated auth tables stay in sync. For Google SSO setup, see the Authentication guide or gencow add sso.

index.ts — API Entry Point

Critical file — the server loads this module and registers only what you list in defineApi. Import CRUD definitions, procedures, routes, and cron schedules from feature modules (for example ./tasks) and register them here:

import "./runtime";

import { cron, defineApi } from "@gencow/core";
import { tasksCrud, searchTasks, cleanupTasks } from "./tasks";

const cleanupTasksCron = cron.name("cleanupTasks").daily({ hour: 3 }).handler(cleanupTasks);

export default defineApi({
    crud: {
        tasks: tasksCrud,
    },
    procedures: {
        searchTasks,
        cleanupTasks,
    },
    crons: { cleanupTasksCron },
});

Pattern: Every createCrud result, custom procedure, httpRoute, and cron schedule must appear under defineApi. The server does not auto-discover files in gencow/ — if it is not imported and registered here, it is not exposed or scheduled.

Cron Jobs

Define scheduled work as procedure.internal handlers, then reference those definitions from independent cron defs in defineApi({ crons }):

import { cron, defineApi } from "@gencow/core";
import { procedure } from "./runtime";

export const syncData = procedure.internal
    .name("data.sync")
    .handler(async ({ context }) => {
        // scheduled server-only work
    });

export const syncDataCron = cron.name("syncData").interval({ minutes: 30 }).handler(syncData);

export default defineApi({
    procedures: { syncData },
    crons: { syncDataCron },
});

Legacy gencow/crons.ts and cronJobs() definitions are still supported for existing projects, but new projects should keep cron registration in the API entry point with cron.

seed.ts — Seed Data (Optional)

Populate the database with test data:

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.

src/gencow/api.ts — Auto-Generated Client (⚡ Do Not Edit)

Generated by gencow dev / gencow codegen into codegen.clientOutDir (default ./src/gencow). Provides type-safe API references for your React frontend:

// src/gencow/api.ts — generated; do not edit manually
import { defineProcedureQuery, defineProcedureMutation } from "@gencow/client";
import type * as Operations from "./operations.d.ts";

export const api = {
    tasks: {
        list: defineProcedureQuery<Operations.TasksListOutput>("tasks.list"),
        get: defineProcedureQuery<Operations.TasksGetOutput>("tasks.get"),
        create: defineProcedureMutation<Operations.TasksCreateOutput>("tasks.create"),
        update: defineProcedureMutation<Operations.TasksUpdateOutput>("tasks.update"),
        remove: defineProcedureMutation<Operations.TasksRemoveOutput>("tasks.remove"),
    },
} as const;

Companion files in the same folder: db.d.ts, operations.d.ts.

gencow/README.md — Auto-Generated AI Guide (⚡ Do Not Edit)

Generated alongside client codegen. 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/uploads/ Uploaded files (via ctx.storage.store())
.gencow/server.js Bundled server copy (for standalone projects)
.gencow/dashboard/ Admin dashboard static assets

Environment Variables

Root .env — frontend build variables

The project-root .env is read by frontend tooling such as Vite. Keep only browser-safe build variables here.

# Frontend → local API
VITE_API_URL=http://localhost:5456

gencow/.env — backend local runtime

Local backend mode loads gencow/.env by default, or the custom envFile from gencow.config.js.

# gencow/.env
MY_BACKEND_API_KEY=replace-me

# Only for a supported local AI direct/fallback path
OPENAI_API_KEY=replace-me

Cloud apps: Local env files are not uploaded automatically. Use gencow env set or review and upload the backend env file with gencow env push.

Cloud environment (gencow env)

Manage environment variables for your deployed app via CLI:

bunx gencow@latest env set MY_BACKEND_API_KEY=replace-me
bunx gencow@latest env list
bunx gencow@latest env push
bunx gencow@latest env push --file ./config/backend.env

Security: Root .env and gencow/.env should be gitignored. Cloud environment variables are encrypted at rest. Never expose backend secrets in frontend code.

Next Steps