Skip to main content

Database Runbook

This runbook covers the migration workflow, Neon branch strategy, and RLS testing for Factory apps.

Architecture

Every Factory app gets an isolated Neon project (not just a schema). This ensures:
  • No cross-app data leakage at the database level
  • Independent scaling and compute pause per app
  • Clean isolation during app transfer to a buyer
Connections from the Cloudflare Worker are routed through a Hyperdrive binding (env.DB) for connection pooling and latency reduction.

Neon branch strategy

Running migrations

Migrations live in src/db/migrations/ as numbered SQL files (001_init.sql, etc.).

Apply manually

Apply via CI

The migrate.yml workflow runs on push to main and applies migrations automatically using the DATABASE_URL GitHub secret.

Inspecting the schema

Factory events table

Each app maintains its own factory_events table inside its own isolated Neon database. The schema is identical across all apps (shared DDL from the scaffold), but there is no central database — each app writes and queries its own events independently via @latimer-woods-tech/analytics. A future factory_core Neon project will aggregate events across all apps when factory-admin is built. Until then, cross-app analytics require querying each app’s database separately.

Row-Level Security (RLS)

Factory apps use RLS to isolate tenant data when multiple tenants share a table.

Enabling RLS

Setting the tenant context per request

Testing RLS in development

Backup and restore

Neon provides continuous branching which doubles as PITR (Point-in-Time Recovery). For explicit backups:

Neon connection strings

Connection strings follow this format:

Getting a WORKING string (you have operator access — use it)

Do not assume you lack DB access, and do not trust the stored *_CONNECTION_STRING secrets blindly — they drift (rotated password not propagated to GCP) and several carry a leading UTF-8 BOM. Mint a fresh one:
Symptoms that mean “stale secret — mint fresh instead of debugging”: password authentication failed (rotated pw) or is not a valid URL (leading BOM — strip with while (cs.charCodeAt(0) !== 0x70) cs = cs.slice(1)). See lessons-learned.md → “You HAVE Neon access” for the full rationale.

Storing for an app

Store in GitHub Secrets as {APP}_CONNECTION_STRING and pass to Wrangler:

Hyperdrive setup