Database

How to add tables, run migrations, and query the database from your app.

All apps share a single Postgres database. Prisma manages the schema (and generates migrations); apps use the pg library at runtime. You never run migrations locally — they're generated and applied automatically on PRs and merges.

The One Schema File

Every table in every app lives in a single file:

shared/database/prisma/schema.prisma

Your app's tables sit alongside tables from every other app. Prefix every app-specific table with the app name in snake_case so ownership is obvious:

// ============================================
// Meal Planner Tables
// ============================================
model MealPlanner_Recipe {
  id        String   @id @default(cuid())
  name      String
  createdAt DateTime @default(now())

  @@map("meal_planner_recipe")
}

The @@map(...) is the actual SQL table name; the Prisma model name is for codegen readability.

See Naming Conventions for the full rules.

Migration Workflow (Zero Local Setup)

You never touch prisma migrate dev or manage .sql files by hand. The pipeline does it:

  1. Edit the schema: add/modify a model in shared/database/prisma/schema.prisma.
  2. Update docs: reflect the change in shared/schema.md — ownership, purpose, relationships.
  3. Commit and push to your feature branch; open a PR to main.
  4. CI auto-generates the migration and commits it back to your PR.
  5. A Neon DB branch is created for the PR, the migration is applied, and the preview app deploys against it.
  6. On merge to main, the migration applies to Neon main and production redeploys.

Prefer additive changes: add new tables and nullable columns instead of modifying existing ones. Schema changes that require data migration need developer review.

Ask Claude to add a new table
Claude prompt
Add a "reservations" table for the meal-planner app. It needs: id, user_email, party_size, start_time, created_at. Follow the monorepo schema conventions and update shared/schema.md.

Querying at Runtime

Use pg via the getDb() helper from the template (app/_lib/db.ts). Always use parameterized queries ($1, $2, …):

Reading datatypescript
import { getDb } from '@/app/_lib/db';

const db = getDb();
const result = await db.query(
  'SELECT * FROM meal_planner_recipe WHERE user_id = $1',
  [userId]
);
const recipes = result.rows;
Writing datatypescript
import { getDb } from '@/app/_lib/db';

const db = getDb();
const result = await db.query(
  'INSERT INTO meal_planner_recipe (name, user_id) VALUES ($1, $2) RETURNING *',
  [name, userId]
);
const newRecipe = result.rows[0];

Never build SQL by string concatenation — that's a SQL injection. $1 / $2 placeholders are safe.

Where You Actually Hit the Database

Database queries only run in PR preview environments and production — never locally. When you open a PR, the pipeline creates an isolated Neon branch with your migrations applied, and the preview deploy connects to it. That's where you test database behavior; the preview URL is posted as a PR comment.

When Something Goes Wrong

If the migration step fails on your PR (red check), see the Database Migration Errors troubleshooting page.

Quiz

Quiz

You added a new model to shared/database/prisma/schema.prisma. What do you do next to get it into the production database?