Skip to Content
Self-hostingDatabase

Database

Screenplay keeps everything durable in Postgres. Any Postgres works. The default driver is Neon’s serverless HTTP driver, and you can swap it.

Setup

  1. Create a database anywhere (Neon, Vercel Postgres, Supabase, your own server) and put its connection string in DATABASE_URL.
  2. That’s all. The checked-in migrations in apps/app/drizzle/ are applied on every build (drizzle-kit migrate && next build), so each deploy upgrades the schema before the new code runs. To set up a local database, run pnpm db:migrate in apps/app.

What’s in the schema

The schema is split in two: apps/app/lib/db/schema-core.ts holds the tables every build uses, and schema-multiuser.ts holds the tables only the hosted build needs. schema.ts combines both.

TablesHolds
userAccounts
session, account, verificationBetter Auth sessions, plus the GitHub OAuth token when hosted
room, room_memberCanvases and, when hosted, who can open them. Membership also gates Yjs and terminal tokens.
folder, room_folder, pinEach user’s folders, filing, and sidebar pins
thread, comment, thread_readHosted Comment threads and per-user read state
agent_chat, agent_message, agent_run, agent_pending_tool_callAgent conversations: chats, their message history, each run’s state, and tool calls waiting on the user (such as a plan awaiting approval)
terminal_tabEach user’s terminal tabs: label, order, and, for tabs opened before shells became plain, which CLI they launch. Never scrollback.
kv_storeKey/value cache with TTLs and locks: model catalogs, encrypted repositories and canvas env vars

Live canvas state (layout, frames, documents, chat tabs) lives in the Yjs host, not in Postgres.

Changing the schema

# 1. Edit apps/app/lib/db/schema-core.ts (or schema-multiuser.ts) # 2. Generate the migration — a SQL file under apps/app/drizzle/ cd apps/app && pnpm db:generate # 3. Commit the .sql file together with the schema change # 4. The next deploy applies it (the build runs `drizzle-kit migrate`)

Migrations are idempotent: already-applied ones are skipped. For throwaway local experiments, pnpm db:push syncs the schema without a migration file. Don’t commit the result.

Preview deploys run migrations against whatever DATABASE_URL their scope has. Point previews at a separate database, or a PR’s migration reaches production before it merges. See Deploying to Vercel.

Using a different Postgres driver

The app programs against the DB type in apps/app/lib/db/types.ts, which is Drizzle’s generic PgDatabase. selectDb() in apps/app/lib/db/index.ts chooses the concrete driver: Neon’s serverless HTTP driver by default, or embedded PGlite when SCREENPLAY_DB=pglite (the desktop build). To use a different driver:

  1. Install it (postgres, pg, @vercel/postgres, …).

  2. Add a sibling factory, for example apps/app/lib/db/postgres-js.ts:

    import postgres from "postgres" import { drizzle } from "drizzle-orm/postgres-js" import * as schema from "./schema" import type { DB } from "./types" export function createPostgresJsDb(): DB { if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is not set") return drizzle(postgres(process.env.DATABASE_URL), { schema }) }
  3. Return it from selectDb() in place of createNeonDb(), or behind a new SCREENPLAY_DB value.

Everything else, including lib/kv, uses the same handle and needs no changes.

Last updated on