Multi-Tenant Patterns
DB9 provisions databases in under a second, each fully isolated with its own TiKV keyspace, credentials, and resource quotas. This makes it practical to use separate databases as the unit of tenancy — something that is usually too expensive or slow with traditional Postgres hosting.
This page compares the four main patterns for organizing tenant data in DB9 and helps you choose the right one for your workload.
Mental model
Section titled “Mental model”Every DB9 database is an isolated tenant.
The server routes connections by parsing the tenant ID from the username ({id}.admin), resolves the tenant to a dedicated TiKV keyspace, and applies per-tenant resource limits (QPS, memory, connections) independently.
There is no shared-schema multi-tenancy inside a single DB9 database — if you need tenant isolation, you create separate databases. Branches inherit the parent’s data at a point in time but run as independent tenants with their own keyspace.
The four patterns below use this isolation primitive in different ways.
Pattern 1: Database-per-user
Section titled “Pattern 1: Database-per-user”What it is: Each end user or customer gets their own database, named deterministically from their identity.
Best for: SaaS platforms, per-user AI agents, personalized workspaces.
import { instantDatabase } from 'get-db9';
const db = await instantDatabase({ name: `user-${userId}`, seed: ` CREATE TABLE preferences (key TEXT PRIMARY KEY, value JSONB); CREATE TABLE history (id SERIAL, action TEXT, ts TIMESTAMPTZ DEFAULT now()); `,});
// db.connectionString — ready to use// db.databaseId — 12-character tenant IDHow it works:
instantDatabase()checks for an existing database with the same name and returns it if found. No duplicates on restart.- The
seedSQL runs only on first creation, so schema setup is idempotent. - Each user’s data is physically isolated — no row-level filtering, no shared pools.
Operational notes:
- Naming convention matters. Use a prefix like
user-ortenant-so you can filter by name when listing or cleaning up. - Credential rotation is per-database. Use
db9 db reset-password <name>or the SDK equivalent. - Fleet cleanup — list all databases and filter by prefix:
const all = await client.databases.list();const userDbs = all.filter(db => db.name.startsWith('user-'));Limits to consider:
- Anonymous accounts are limited to 5 databases. For production use, claim your account first.
- Each database carries its own resource quotas (QPS, memory). Under heavy concurrent usage, monitor per-tenant resource consumption.
Pattern 2: Database-per-app
Section titled “Pattern 2: Database-per-app”What it is: Each application, service, or environment gets one shared database. All users of that application share the same database.
Best for: Monolithic apps, shared data models, internal tools, prototypes.
import { instantDatabase } from 'get-db9';
const db = await instantDatabase({ name: 'myapp-production', seed: ` CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT UNIQUE); CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id)); `,});How it works:
- One database per logical environment (dev, staging, production).
- All application users connect through the same credentials.
- Schema and data are shared — traditional application-level multi-tenancy applies if needed.
Operational notes:
- This is the simplest pattern. Start here if you don’t need per-user isolation.
- You can still use DB9 branching for safe testing against production data.
- Resource limits (QPS, memory, connections) apply to the single database, so all users share the quota.
When this pattern breaks down:
- When you need hard isolation between customers for compliance or security.
- When per-user resource limits matter — a noisy user affects everyone in the same database.
Pattern 3: Ephemeral-per-task
Section titled “Pattern 3: Ephemeral-per-task”What it is: A database is created for a specific task (CI run, agent session, one-shot job) and deleted when the task finishes.
Best for: CI/CD test isolation, agent task runners, batch processing, disposable sandboxes.
CLI pattern
Section titled “CLI pattern”# Create a database for this CI runRESULT=$(db9 create --name "ci-${BUILD_ID}" --show-connection-string --json)DB_ID=$(echo "$RESULT" | jq -r '.id')CONN=$(echo "$RESULT" | jq -r '.connection_string')
# Run testsDATABASE_URL="$CONN" npm test
# Clean updb9 delete "$DB_ID" --yesSDK pattern
Section titled “SDK pattern”import { instantDatabase, createDb9Client } from 'get-db9';
const taskId = crypto.randomUUID();const db = await instantDatabase({ name: `task-${taskId}`, seed: 'CREATE TABLE results (id SERIAL, data JSONB)',});
try { // ... do work ...} finally { const client = createDb9Client(); await client.databases.delete(db.databaseId);}Operational notes:
- Always clean up. DB9 does not automatically delete idle databases. Build cleanup into your task lifecycle.
- Naming conventions help cleanup. If tasks crash before deletion, you can sweep stale databases by prefix and age:
const client = createDb9Client();const all = await client.databases.list();const stale = all.filter(db => db.name.startsWith('ci-') && new Date(db.created_at) < oneDayAgo);for (const db of stale) { await client.databases.delete(db.id);}- Anonymous account limit: If running many concurrent tasks from an anonymous account, you may hit the 5-database limit. Claim your account or clean up between runs.
Pattern 4: Branch-per-preview
Section titled “Pattern 4: Branch-per-preview”What it is: A branch is created from a parent database to test changes against a copy of real data, then deleted when the preview is done.
Best for: Preview environments, schema migration testing, rollback validation, feature development.
CLI pattern
Section titled “CLI pattern”# Create a branch from the production databasedb9 branch create production --name "preview-pr-42"
# Test migrations against the branchdb9 db connect preview-pr-42 < migration.sql
# Verifydb9 db connect preview-pr-42 -c "SELECT count(*) FROM users"
# Clean updb9 delete preview-pr-42 --yesREST API pattern
Section titled “REST API pattern”# Create a branch via the REST APIcurl -X POST "https://api.db9.ai/customer/databases/${PARENT_DB_ID}/branch" \ -H "Authorization: Bearer $DB9_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "preview-pr-42"}'
# Returns a DatabaseResponse with its own connection_string and IDHow branching works:
- A branch creates a new database that starts with a snapshot of the parent’s data.
- The branch runs as an independent tenant — writes to the branch do not affect the parent.
- Branch creation transitions through
CREATING → CLONING → ACTIVE. - Once active, the branch behaves exactly like any other database.
- Branches count toward your database limit.
Operational notes:
- No merge-back. Branches are one-way copies. There is no built-in mechanism to merge branch changes back to the parent.
- No live sync. The branch is a point-in-time snapshot. Changes to the parent after branching are not reflected.
- Use for validation, not long-lived environments. Branches are best for short-lived testing. For persistent environments, use separate named databases.
Choosing a pattern
Section titled “Choosing a pattern”| Concern | Database-per-user | Database-per-app | Ephemeral-per-task | Branch-per-preview |
|---|---|---|---|---|
| Isolation | Full | None (shared DB) | Full | Full (snapshot copy) |
| Setup cost | One per user | One per environment | One per task | One per preview |
| Data lifecycle | Persistent | Persistent | Disposable | Disposable |
| Cleanup needed | On user deletion | On app shutdown | On task completion | On preview close |
| Schema management | Per-database seeds | Shared migrations | Per-task seeds | Inherited from parent |
| Best scale | Hundreds to thousands | One to a few | Hundreds concurrent | Tens concurrent |
Mixing patterns
Section titled “Mixing patterns”Most production deployments combine two or more patterns:
- Database-per-user + branch-per-preview: Each user has a persistent database; branches test schema changes before applying to user databases.
- Database-per-app + ephemeral-per-task: The application uses one shared database, but CI runs use disposable databases for test isolation.
- Database-per-user + ephemeral-per-task: Each user has a persistent database, but agent tasks create temporary databases for sandboxed work.
Isolation and resource boundaries
Section titled “Isolation and resource boundaries”Every DB9 database — whether created directly, per-user, or as a branch — runs with these isolation properties:
| Boundary | Scope | Detail |
|---|---|---|
| TiKV keyspace | Per-database | Each database writes to db9_tenant_{id} — no shared storage |
| Credentials | Per-database | Each database has its own admin password |
| Connection limits | Per-user within database | PostgreSQL rolconnlimit enforced per user |
| QPS limit | Per-database | Token-bucket rate limiter (configurable, disabled by default) |
| Memory quota | Per-database | Per-statement memory accounting with RAII cleanup |
| Idle eviction | Per-database | Tenant state evicted from server memory after 5 minutes idle |
No cross-database queries. Each database is a separate tenant. There is no dblink, foreign data wrapper, or cross-database join support.
Constraints and boundaries
Section titled “Constraints and boundaries”- No shared-schema multi-tenancy. DB9 does not support schema-per-tenant or row-level security within a single database as a tenancy mechanism. Use separate databases instead.
- No database rename. Names are permanent. Choose naming conventions carefully.
- No region migration. Databases cannot be moved between regions after creation.
- Branch limits. Branches count toward your database limit. Clean up unused branches to stay within quota.
- No merge-back from branches. Branch changes cannot be automatically applied to the parent database.
- Idle eviction is server-side only. A database’s in-memory state (connection pool, caches) is evicted after 5 minutes of inactivity. The data persists in TiKV — only the server-side tenant handle is released.
Next steps
Section titled “Next steps”- Provisioning — how databases are created, listed, and deleted
- Architecture — how tenant isolation works at the TiKV and pgwire level
- Agent Workflows — how provisioning and tenancy fit the agent lifecycle
- Production Checklist — secrets, auth, and operational readiness
- Anonymous and Claimed Databases — trial-first usage and account upgrade paths (coming soon)
- Branching Workflows — deeper guide on preview, rollback, and task-isolation flows (coming soon)