Database
Prisma schema, migrations, and seeding on PostgreSQL.
Last updated on
4 min readSyntaxKit uses PostgreSQL via Prisma, owned by packages/database. Bring any Postgres you like: a hosted provider such as Neon, Supabase, or RDS, or one you run yourself with Docker. The same DATABASE_URL switches the app between them, and the Prisma client picks the right driver adapter automatically.
Choose a Postgres
Point DATABASE_URL at a hosted provider or a local Docker container.
Schema layout
How the Prisma schema is split across domain files.
Common workflows
Migrate, reset, seed, inspect, and regenerate the client.
Migrations in production
The non-interactive migrator and how to run it safely.
Choose A Postgres
Create a Postgres database on Neon, Supabase, AWS RDS, or any other Postgres provider, then paste the connection string into DATABASE_URL:
DATABASE_URL="postgresql://<user>:<password>@<host>/<database>"*.neon.tech URLs automatically use the @prisma/adapter-neon serverless adapter (HTTP-friendly, no socket pooling). Every other host falls back to the standard @prisma/adapter-pg driver. Your application code is identical either way.
For local development, the simplest path is a one-off Postgres container:
docker run --name syntaxkit-postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=syntaxkit \
-p 5432:5432 \
-d postgres:16Then point DATABASE_URL at it:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/syntaxkit"For production self-hosting, the docker-compose.yml at the repo root does not bundle a Postgres service. Provide a DATABASE_URL to a managed instance (Neon, Supabase, RDS, ...) or a separate self-hosted Postgres, then run the bundled migrator service to apply schema migrations before bringing up web.
Schema Layout
The Prisma schema is split across several files in packages/database/prisma/ so each domain has a clear home:
What's Modeled
Identity and organizations
User, Session, Account, Organization, Member, Invitation, TwoFactor, Passkey, Verification. Owned by Better Auth and regenerated automatically.
Billing
Subscription mirrors the Stripe state per organization; StripeWebhookEvent records each webhook for idempotent processing.
AI
Chat and Message hold per-organization conversations; AiUsageEvent is the ledger used for quotas and analytics.
Side-effect idempotency
OutboundEffect deduplicates outgoing emails and analytics by a semantic key, so retries cannot double-send.
How The Models Connect
The diagram below shows the foreign-key relationships across the four domains.
Standalone tables (Verification, StripeWebhookEvent, OutboundEffect) are intentionally omitted because they are written and read by name, not by relation.
The diagram source lives at apps/docs/diagrams/database-erd.mmd. Rerun pnpm --filter @syntaxkit/docs diagrams:build after editing it to refresh both SVG variants.
Two patterns are worth calling out: every product entity (Subscription, Chat, AiUsageEvent) is scoped to an Organization rather than a User, and User is wired into orgs only through Member. That is what makes SyntaxKit multi-tenant by default.
Common Workflows
Expand the task you're doing.
Add or change a model
Edit the right schema in packages/database/prisma/models/, then create a migration:
pnpm db:migrate:devPrisma prompts for a name; pick something descriptive (add_team_invitations, not update).
Apply pending migrations
On a fresh checkout, run:
pnpm db:migrate:devThis applies every migration in prisma/migrations/ and regenerates the typed client. The :dev suffix matches prisma migrate dev and is intended for development only; production deploys use pnpm db:migrate:deploy (see Migrations In Production).
Reset and reseed locally
Wipe the local database and start clean:
pnpm db:resetOr wipe and load demo data in one shot:
pnpm db:reset:demoInspect data
Open the Prisma Studio GUI:
pnpm db:studioRegenerate the typed client
pnpm db:generatepnpm install no longer triggers prisma generate. Run pnpm db:generate after any hand-edit to a *.prisma file or a fresh clone, before pnpm dev.
Regenerate Better Auth-owned models
pnpm auth:generateNever edit auth.generated.prisma by hand. Update packages/auth/src/server.ts instead and rerun this script.
Using The Client In Your Code
Import the singleton client from @syntaxkit/database and write Prisma queries the usual way:
import { prisma } from "@syntaxkit/database";
const orgs = await prisma.organization.findMany({
where: { members: { some: { userId } } },
});prisma is a process-wide singleton. The driver adapter is chosen for you based on DATABASE_URL (Neon serverless when the host ends with .neon.tech, standard pg everywhere else), so your application code never has to know which Postgres it is talking to.
Migrations In Production
Production deploys use the non-interactive migrator:
pnpm db:migrate:deployTwo important details:
- The
migratorservice indocker-compose.ymlruns this command for self-hosted Docker deploys. Run it before bringing upwebafter every schema change. pnpm db:migrate:statusreports whether the database is in sync with the migrations directory; useful in CI and on deploy targets that do not auto-migrate.
For platform-specific runbooks (Vercel, Fly.io, Render, Docker), see Deployment and Going To Production.
Seeding
prisma db seed runs through packages/database/prisma/seed.ts, which dispatches to one of three modes:
bootstrap
Default. Leaves the database clean so first sign-up creates the personal organization. Run with pnpm db:seed:bootstrap or just pnpm db:seed.
demo
Sample organizations and an admin (admin@demo.syntaxkit.com / password123). Run with pnpm db:seed:demo.
test
Fixtures used by pnpm test:integration. Run with pnpm db:seed:test.
Where To Go Next
Authentication
Better Auth wiring, sessions, and the models in auth.generated.prisma.
Billing
How Subscription mirrors Stripe and how webhooks stay idempotent.
AI
Chat, messages, and the usage event ledger used for quotas.
Deployment
Per-target setup for Vercel, Fly.io, Render, and Docker.
Going To Production
The pre-launch checklist, including running migrations safely.
