Skip to content
Discord Get Started

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.

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.

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.

TypeScript
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 ID

How it works:

  • instantDatabase() checks for an existing database with the same name and returns it if found. No duplicates on restart.
  • The seed SQL 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- or tenant- 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:
TypeScript
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.

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.

TypeScript
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.

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.

Terminal
# Create a database for this CI run
RESULT=$(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 tests
DATABASE_URL="$CONN" npm test
# Clean up
db9 delete "$DB_ID" --yes
TypeScript
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:
TypeScript
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.

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.

Terminal
# Create a branch from the production database
db9 branch create production --name "preview-pr-42"
# Test migrations against the branch
db9 db connect preview-pr-42 < migration.sql
# Verify
db9 db connect preview-pr-42 -c "SELECT count(*) FROM users"
# Clean up
db9 delete preview-pr-42 --yes
Terminal
# Create a branch via the REST API
curl -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 ID

How 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.
ConcernDatabase-per-userDatabase-per-appEphemeral-per-taskBranch-per-preview
IsolationFullNone (shared DB)FullFull (snapshot copy)
Setup costOne per userOne per environmentOne per taskOne per preview
Data lifecyclePersistentPersistentDisposableDisposable
Cleanup neededOn user deletionOn app shutdownOn task completionOn preview close
Schema managementPer-database seedsShared migrationsPer-task seedsInherited from parent
Best scaleHundreds to thousandsOne to a fewHundreds concurrentTens concurrent

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.

Every DB9 database — whether created directly, per-user, or as a branch — runs with these isolation properties:

BoundaryScopeDetail
TiKV keyspacePer-databaseEach database writes to db9_tenant_{id} — no shared storage
CredentialsPer-databaseEach database has its own admin password
Connection limitsPer-user within databasePostgreSQL rolconnlimit enforced per user
QPS limitPer-databaseToken-bucket rate limiter (configurable, disabled by default)
Memory quotaPer-databasePer-statement memory accounting with RAII cleanup
Idle evictionPer-databaseTenant 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.

  • 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.