Database Migrations
Deploy-request workflow, branch lifecycle, and the migration TUI.
OpenInsure uses PlanetScale Vitess 22.0 as its primary database, accessed from Cloudflare Workers via 6 Hyperdrive bindings for connection pooling and read-query caching at the edge. The ORM is Drizzle 0.45+ with a schema-first approach — all tables are defined in TypeScript and migrations are generated by the Drizzle CLI.
The database is split into 5 domain keyspaces (vertical split, cutover 2026-03-27): oi_global, oi_policies, oi_submissions, oi_claims, oi_billing. Each keyspace has its own connection pool (Hyperdrive binding) and its own deploy-request lifecycle.
packages/db/├── src/│ ├── schema/ # Drizzle table definitions│ │ ├── policies.ts # one file per table family│ │ ├── claims.ts│ │ ├── ...│ │ └── domain/│ │ ├── global.ts # oi_global keyspace barrel│ │ ├── policies.ts # oi_policies keyspace barrel│ │ ├── submissions.ts # oi_submissions keyspace barrel│ │ ├── claims.ts # oi_claims keyspace barrel│ │ └── billing.ts # oi_billing keyspace barrel│ ├── domain-clients.ts # createGlobalDb / createPoliciesDb / etc.│ └── index.ts # Legacy createDb() (monolithic)├── migrations/│ ├── mysql/ # PRIMARY: drizzle-kit-generated SQL files│ │ └── meta/_journal.json # Migration tracking│ └── baseline/ # Full schema snapshot for fresh databases├── drizzle.config.ts├── INFRASTRUCTURE.md # Internal: topology + connection paths└── SECURITY.md # Internal: credential managementEach domain has its own Hyperdrive binding. The factory functions in domain-clients.ts return a typed Drizzle instance scoped to that keyspace.
import { createGlobalDb, createPoliciesDb, createClaimsDb, createSubmissionsDb, createBillingDb,} from '@openinsure/db';
// In a Hono handler (apps/api/src/routes/...):const policiesDb = createPoliciesDb(env.HYPERDRIVE_POLICIES);const policies = await policiesDb .select() .from(schema.policies) .where(eq(schema.policies.orgId, orgId)); // orgId scoping is mandatoryThe middleware in apps/api/src/middleware/database.ts injects db, globalDb, policiesDb, submissionsDb, claimsDb, billingDb, and orgId onto every request context. Routes pull what they need from there.
PlanetScale Vitess does not support row-level security. Tenant isolation is enforced at the application layer: every multi-tenant query MUST include eq(table.orgId, orgId).
// Correctconst polices = await policiesDb .select() .from(schema.policies) .where(eq(schema.policies.orgId, orgId));
// Incorrect — would return all tenants' dataconst policies = await policiesDb.select().from(schema.policies);The orgId value comes from the JWT, never from user input. Cross-org roles (superadmin, system, auditor) may pass ?orgId=<uuid> as a query parameter; normal users cannot override their JWT orgId. The middleware enforces this distinction.
The vertical-split cutover completed 2026-03-27. VTGate routes queries to the correct keyspace via MoveTables routing rules established during the cutover. The legacy monolithic HYPERDRIVE binding still works but new code should use the domain-specific bindings.
| Keyspace | Tables | Domain | Schema barrel |
|---|---|---|---|
oi_global | 53 | Orgs, users, producers, rules, programs, reference data | packages/db/src/schema/domain/global.ts |
oi_policies | 28 | Policies, endorsements, coverages, drivers, vehicles | packages/db/src/schema/domain/policies.ts |
oi_submissions | 33 | Submissions, quotes, motor carriers, FMCSA, MVR, loss runs | packages/db/src/schema/domain/submissions.ts |
oi_claims | 29 | Claims, claimants, disbursements, settlement, subrogation | packages/db/src/schema/domain/claims.ts |
oi_billing | 47 | Billing, payments, GL, chart of accounts, reinsurance | packages/db/src/schema/domain/billing.ts |
Total: 190 tables as of 2026-04-06. Each keyspace is currently unsharded (NUM_SHARDS=1). The future sharding plan (when needed) is org_id via an xxhash vindex on the four tenant-scoped keyspaces. oi_global stays unsharded forever — its reference tables (organizations, programs, producers, platform_carriers) must be co-located with every shard.
eq(table.orgId, orgId) — no RLS in MySQLmain goes through deploy requests — safe migrations are enabled and direct DDL is rejectedIF NOT EXISTS on ALTER TABLE__drizzle_migrations will mismatch and block all future migrations| Branch | Purpose |
|---|---|
main | Production. Safe migrations enabled — direct DDL is rejected; all changes via deploy requests |
ci-test | Long-lived shared dev branch. Direct DDL allowed. |
<feature-*> | Transient per-PR branches. Created from main, used to author one migration, deleted post-DR. |
Always pass --auto-delete-branch when creating a deploy request — it removes the source branch automatically when the DR completes, which keeps the dev-branch hour count under the monthly cap.
packages/db/src/schema/<table>.tspackages/db/src/schema/domain/<keyspace>.tsmake db-generate — drizzle-kit creates a new SQL file in packages/db/migrations/mysql/ (named <NNNN>_<slug>.sql)--> statement-breakpoint between statementsmake vitess-up && make db-migrate — apply locally to confirmpscale deploy-request create ai --from <branch> --into main --notes "..." --auto-delete-branchpscale deploy-request deploy ai NNpscale dr revert ai NN if neededTwo drivers, one Drizzle dialect (mode: 'planetscale'):
| Environment | Driver | Why |
|---|---|---|
| Workers | @planetscale/database (HTTP fetch) | No raw TCP from a Worker |
| Local dev | mysql2 | Talks to docker-compose Vitess testserver |
| Tests | mysql2 | Same as local dev |
Selection happens automatically in packages/db/src/domain-clients.ts based on the connection string format. Workers see a Hyperdrive binding object exposing connectionString, host, user, password, database — never raw PlanetScale credentials.
For the full Hyperdrive story (the 6 bindings, query caching behavior, smart placement, troubleshooting), see the Hyperdrive page.
make vitess-up # docker-compose vitess testserver — 5 keyspaces, port 13306make vitess-shell KS=oi_global # mycli into a specific keyspacemake vitess-status # table counts per keyspacemake vitess-reset # wipe + re-init after schema driftThe local Vitess testserver (vitess/vttestserver:v21.0.2-mysql80) mirrors the production 5-keyspace topology exactly. Docker compose definition lives in docker-compose.yml.
Database Migrations
Deploy-request workflow, branch lifecycle, and the migration TUI.
Hyperdrive
The 6 bindings, query caching, smart placement, and credential lifecycle.
Packages Overview
All @openinsure/* packages, exports, and consumers.