Neon Multi-Tenant Skill
When to Use
Invoke this skill when the task involves any of the following:
- •Neon branch strategy for multi-tenant or multi-app isolation
- •Schema organization across shared databases (e.g., shared auth + per-app schemas)
- •Drizzle ORM configuration with tenant-scoped queries and automatic
tenant_idinjection - •Cloudflare Hyperdrive connection pooling for Neon in Workers
- •Migration coordination across Neon branches
- •Choosing isolation level: branches vs schemas vs RLS for different tenant tiers
This skill is Neon PostgreSQL-specific. For general multi-tenant architecture patterns, see multi-tenant-platform. For auth-specific patterns, see graduated-auth.
Core Concept: Neon Branches as Isolation Primitives
Neon's branch architecture maps naturally to multi-tenant isolation. A branch is a copy-on-write fork of the database at a point in time. Branches share storage for unchanged data, making them cost-effective for isolation.
production (default branch)
|-- neon_auth schema (shared Better Auth tables)
|-- platform schema (T0 management plane)
|
+-- app-alpha-prod (branch)
| |-- app_alpha schema (T1 partner data)
| |-- neon_auth schema (inherited, read-only reference)
| |
| +-- app-alpha-dev (branch of app-alpha-prod)
| |-- development copy for safe iteration
|
+-- app-beta-prod (branch)
|-- app_beta schema (T1 partner data)
|-- neon_auth schema (inherited, read-only reference)
|
+-- app-beta-dev (branch of app-beta-prod)
|-- development copy for safe iteration
Key properties:
- •Copy-on-write: Branches are near-instant to create and share unchanged pages with the parent.
- •Independent compute: Each branch can have its own compute endpoint, enabling per-tenant scaling.
- •Schema inheritance: A child branch inherits the parent's schema at creation time. Auth tables in
neon_authare present on all branches. - •Instant reset: Dev branches can be reset to their parent's state without a full restore.
SyncUpSuite Branch Layout
The SyncUp monorepo uses a single Neon project (your-neon-project, ID: your-neon-project-id) with the following branch hierarchy:
production (default)
|-- neon_auth schema (Better Auth: users, sessions, accounts, verifications)
|
+-- brandsyncup-prod (br-xxx-xxx-xxxxxxxx)
| |-- brandsyncup schema (brand collaboration data)
| +-- brandsyncup-dev (br-xxx-xxx-xxxxxxxx)
|
+-- legalsyncup-production (br-xxx-xxx-xxxxxxxx)
|-- legalsyncup schema (legal compliance data)
+-- legalsyncup-development (br-xxx-xxx-xxxxxxxx)
- •Shared auth: All apps authenticate against
neon_authon the production branch. Session tokens are portable across apps. - •Isolated data: Each app's domain tables live in app-specific schemas on app-specific branches.
- •Dev branches: Feature development and schema iteration happen on dev branches that can be reset from their parent.
Schema Organization
The Three Schema Tiers
| Schema | Scope | Owner | Branch |
|---|---|---|---|
neon_auth | Cross-app authentication | Better Auth | production (default) |
public | Neon system tables, extensions | PostgreSQL | all branches |
<app_name> | Application-specific domain data | Application team | app branch |
Rules
- •
Never put app data in
neon_auth. That schema is owned by Better Auth and shared across all apps. Adding app-specific columns to auth tables creates coupling. - •
Always use explicit schema names. Never rely on
search_pathfor multi-schema databases. In Drizzle, specifyschemaon every table definition. - •
Extensions go in
public. PostgreSQL extensions (uuid-ossp,pgcrypto, etc.) should be created in thepublicschema on the production branch so they propagate to all child branches. - •
Every app table has
tenant_id. Even within an app-specific schema on an app-specific branch, tables must includetenant_idfor RLS. This is defense-in-depth: the branch provides infrastructure isolation, andtenant_id+ RLS provides row-level isolation within that branch.
Isolation Decision Matrix
Use this matrix to decide which isolation mechanism to apply at each tier of the 3-tier model:
| Tier | Isolation Mechanism | When to Use | Neon Implementation |
|---|---|---|---|
| T0 (Platform) | Separate Neon project | Always -- the management plane must be fully isolated | Dedicated Neon project with its own connection string |
| T1 (Partner) | Neon branch per partner | Partners with regulatory requirements, data residency needs, or SLA guarantees | neon branches create --parent production per partner |
| T1 (Partner, lightweight) | Schema per partner within shared branch | Partners that share infrastructure and trust the platform for isolation | CREATE SCHEMA partner_name within a shared branch |
| T2 (Customer) | RLS within branch/schema | Always -- customers within a partner's branch are isolated via tenant_id + RLS policies | ALTER TABLE ... ENABLE ROW LEVEL SECURITY + policies |
Decision Flow
Is this a platform-level concern (billing, partner management)?
YES -> T0 separate project
NO -> Does this partner need strong isolation (regulatory, SLA, data residency)?
YES -> T1 dedicated Neon branch
NO -> T1 shared branch with schema isolation
-> T2 always uses RLS within the branch
Hybrid Example: SyncUpSuite
SyncUpSuite currently operates with a simplified model:
- •T0: Platform management is in the production (default) branch.
- •T1: Each app (BrandSyncUp, LegalSyncUp) is a T1 partner with its own branch.
- •T2: Within each app, customer organizations are isolated via
tenant_id+ RLS.
As the platform grows, a T1 partner (e.g., an enterprise customer running their own BrandSyncUp instance) could be given their own Neon branch forked from brandsyncup-prod.
Drizzle ORM Tenant Configuration
Tenant-Scoped Client
Every database query must execute within a tenant context. The pattern is:
- •Create a base Drizzle client connected to the correct branch.
- •Wrap it in a tenant-scoped factory that injects
tenant_idinto all queries. - •Use Drizzle's
$onQueryor middleware hooks for automatic injection.
See templates/drizzle-tenant.ts for the full implementation.
Key Patterns
// Executing a tenant-scoped query (context + query in one transaction)
const docs = await tenantQuery(connectionString, { tenantId, userId }, async (db) => {
return db.select().from(documents); // RLS enforces tenant_id filter
});
// Inserts with automatic tenant_id injection
await db.insert(documents).values({ title: 'New Doc' }); // tenant_id injected
Connection Strategy
| Environment | Connection Method | Configuration |
|---|---|---|
| Production (Workers) | Hyperdrive | env.HYPERDRIVE.connectionString |
| Development (local) | Neon serverless driver | Direct connection string from Doppler |
| Testing | Neon serverless driver | Dev branch connection string |
| Drizzle Studio | Direct TCP | ?sslmode=require on dev branch URL |
Hyperdrive Connection Pooling
Cloudflare Hyperdrive provides connection pooling at the edge, eliminating cold-start connection overhead for Neon. This is critical for Workers because:
- •Each Worker invocation would otherwise create a new TCP + TLS connection to Neon.
- •Neon's serverless driver uses WebSocket, but Hyperdrive uses persistent TCP connections from Cloudflare's network.
- •Hyperdrive caches query results at the edge (configurable).
Performance Impact
| Metric | Without Hyperdrive | With Hyperdrive |
|---|---|---|
| Connection setup | 50-150ms (TLS + auth) | ~0ms (pooled) |
| First query latency | 80-200ms | 10-30ms |
| Sustained query latency | 20-50ms | 10-30ms |
See templates/hyperdrive-setup.md for configuration details.
Migration Patterns
Schema Changes Across Branches
Neon branches are point-in-time forks. Schema changes on a parent do NOT automatically propagate to existing child branches. This requires a deliberate migration strategy:
- •Apply to parent first. Run Drizzle migrations on the production (or app-prod) branch.
- •Reset dev branches. After parent migration, reset dev branches to inherit the new schema:
neon branches reset <dev-branch-id> --parent. - •Or migrate dev independently. For in-progress features, apply the same migration to the dev branch directly.
Shared Auth Schema Migrations
Changes to neon_auth tables affect all apps. The coordination protocol:
- •Draft the migration in a feature branch.
- •Review with all app teams (BrandSyncUp + LegalSyncUp).
- •Apply to production (default) branch during a maintenance window.
- •Reset or migrate all app branches.
- •Verify auth flows in all apps.
Drizzle Migration Commands
# Generate migration from schema changes npx drizzle-kit generate # Push schema directly (dev only, no migration files) npx drizzle-kit push # Apply migrations (production) npx drizzle-kit migrate # Open Drizzle Studio for visual inspection npx drizzle-kit studio
Neon API for Branch Management
Branch lifecycle operations use the @neondatabase/api client:
import { createApiClient } from '@neondatabase/api';
const neon = createApiClient({ apiKey: process.env.NEON_API_KEY });
// Create a new branch for a T1 partner
const branch = await neon.createProjectBranch(projectId, {
branch: {
name: `partner-${partnerId}-prod`,
parent_id: productionBranchId,
},
endpoints: [{ type: 'read_write' }],
});
// Reset a dev branch to match its parent
await neon.restoreProjectBranch(projectId, devBranchId, {
source_branch_id: parentBranchId,
});
See templates/branch-strategy.sql for full examples.
Non-Negotiable Rules
- •
Every query runs in tenant context. No database query executes without an explicit
tenant_idbinding, whether via Drizzle middleware, RLSSETvariable, or explicitWHEREclause. - •
Auth tables are read-only from app branches. Apps read from
neon_authbut never write directly. All auth mutations go through Better Auth's API on the production branch connection. - •
Dev branches are ephemeral. Never store data on dev branches that cannot be recreated. Dev branches can be reset at any time.
- •
Connection strings are secrets. Never hardcode Neon connection strings. Use Doppler for injection and Hyperdrive bindings in production.
- •
Migrations are forward-only. Never run
DROPor destructive DDL without a rollback plan. Prefer additive migrations (add column, add table) over destructive ones. - •
Branch names are deterministic. Use the pattern
<app>-<environment>(e.g.,brandsyncup-prod,legalsyncup-development). For T1 partner branches, use<app>-partner-<id>-<env>.
Templates
| File | Purpose |
|---|---|
templates/branch-strategy.sql | Branch hierarchy, schema setup, Better Auth tables, Neon API examples |
templates/drizzle-tenant.ts | Tenant-scoped Drizzle client factory with automatic tenant_id injection |
templates/hyperdrive-setup.md | Cloudflare Hyperdrive configuration and usage |
References
| File | Purpose |
|---|---|
references/neon-auth-schema.md | Detailed neon_auth schema documentation and cross-app auth flows |
Related Skills
- •
multi-tenant-platform-- General multi-tenant architecture, 3-tier model, governance rules - •
graduated-auth-- Progressive authentication complexity with Better Auth - •
theme-inspired-tokens-- Design token generation for multi-tenant theming