tenant-postgres Documentation

Complete, production-ready guide for the @node-tenant/tenant-postgres package.

1. Introduction

What is `tenant-postgres`?
It is a powerful database adapter for the @node-tenant ecosystem designed exclusively for PostgreSQL. It uses the official pg package to manage the creation, deletion, migration, and connection pooling for tenant databases/schemas.

What problem it solves:
Managing physical tenant boundaries in PostgreSQL is complex. You have to create users, schemas or databases, run migrations per tenant, and safely route queries during HTTP requests without leaking data. tenant-postgres standardizes this entire lifecycle using robust SQL implementations.

Tip: Actual Isolation Strategy Implemented: Unlike many generic wrappers, tenant-postgres natively supports both Schema-per-tenant (strategy: 'schema') AND Database-per-tenant (strategy: 'database'). It dynamically executes different PostgreSQL logic depending on the configuration of the active tenant.
  • Schema Strategy ('schema'): Creates a PostgreSQL schema named after the tenant slug (CREATE SCHEMA "<slug>") in a shared database. It switches contexts by executing SET search_path TO "<slug>".
  • Database Strategy ('database'): Creates an entirely separate physical database (CREATE DATABASE "tenant_<slug>" TEMPLATE template1). It switches contexts by pulling a dedicated connection from a custom pool factory.

Real-world Analogy

Think of PostgreSQL as an office building.
If a tenant is on the Schema Strategy, they are given a private, locked filing cabinet (Schema) inside the shared main lobby (Admin Database). When they ask for files, you hand them a generic lobby key but restrict their view to their cabinet (SET search_path).
If they are on the Database Strategy, they are given a completely separate, standalone building across the street (Tenant Database). You have to keep a totally different set of keys (Tenant Connection Pool) just to enter their building.

Warning: Responsibilities: tenant-postgres natively provides database/schema creation, dropping, `.sql` file migration execution, and connection switching. The consuming application must implement tenant HTTP resolution (e.g., parsing subdomains), initializing the pg connection pools, and providing the SQL migration files.

2. Prerequisites

Before using tenant-postgres, your environment must meet the following requirements:

  • Node.js: Version 16.x or higher.
  • Package Manager: npm, pnpm, or yarn.
  • PostgreSQL: A running PostgreSQL server. No special extensions are strictly required by the adapter itself.
  • pg (node-postgres): Version >= 8.0.0 is required as a peer dependency.
  • Database Permissions: If using the 'database' strategy, the database user connecting to the admin pool MUST have CREATEDB privileges. If using 'schema', the user must have CREATE privileges on the shared database.
  • @node-tenant/tenant-core: Required for the Tenant and DatabaseAdapter interface definitions.

3. Project Structure

The internal structure of the tenant-postgres package is highly focused:

packages/tenant-postgres/
├── src/
└── index.ts // Core logic (createPostgresDatabaseAdapter)
├── package.json // Dependencies and peer dependency definitions (pg)
├── tsup.config.ts // Build configuration
  • src/index.ts: The single source of truth. It exports the factory createPostgresDatabaseAdapter which returns an object satisfying the DatabaseAdapter interface. It implements connection pool handling and dynamic raw SQL generation for both strategies. Developers should not modify this package file directly.

4. Creating a New Tenant

When your application (or control plane) registers a new tenant, you pass the `Tenant` object to the adapter. The adapter then provisions the database structure based on the tenant's configured strategy.

Step-by-step Provisioning:

  1. Tenant Creation: You create a Tenant record in your primary control-plane database (e.g. using tenant-core). The record includes slug and strategy (either 'schema' or 'database').
  2. Provisioning: You call await dbAdapter.createTenantDatabase(tenant).
    - If schema: executes CREATE SCHEMA IF NOT EXISTS "slug".
    - If database: connects outside a transaction and executes CREATE DATABASE "tenant_slug" TEMPLATE template1.
  3. Migrations: You call await dbAdapter.migrateTenant(tenant, './migrations'). The adapter reads .sql files, switches to the correct tenant context, and executes the SQL to build tables.
  4. Seeding: Optionally, you call await dbAdapter.seedTenant(tenant, async (client) => { ... }).
typescript
import { Pool } from 'pg';
import { createPostgresDatabaseAdapter } from '@node-tenant/tenant-postgres';

const adminPool = new Pool({ connectionString: process.env.PG_ADMIN_URL });

const dbAdapter = createPostgresDatabaseAdapter({
  adminPool,
  // Required if using 'database' strategy to connect to the new physical DB
  getTenantPool: (slug) => new Pool({ connectionString: `postgres://user:pass@host/tenant_${slug}` }),
});

// Example provisioning function in your app
async function onTenantSignup(tenantData) {
  const tenant = { id: 1, slug: 'acme', strategy: 'schema' }; // Mock tenant
  
  // 1. Creates the Schema
  await dbAdapter.createTenantDatabase(tenant);
  
  // 2. Runs table creation scripts from folder
  await dbAdapter.migrateTenant(tenant, './src/migrations');
  
  console.log("Tenant created!");
}

5. Database Setup

To utilize tenant-postgres, you must configure standard pg connection pools and provide them to the adapter.

  • Admin Pool: Passed via options.adminPool. This connects to your primary database (often named postgres or your app's main DB). It is used to execute CREATE DATABASE and to serve all queries for schema-strategy tenants.
  • Tenant Pool Factory: Passed via options.getTenantPool. If you use the database strategy, the adapter calls this function to get a dedicated Pool for tenant_<slug>.
  • Template Database: When using the database strategy, Postgres creates the new database by cloning a template. By default, this is template1, but you can override it via options.template.

Native Migrations & Seeding

Tip: Migrations ARE natively supported! Unlike many ORM wrappers, this adapter includes a lightweight, raw SQL migration runner specifically designed for multi-tenancy.

How migrations work: When you call migrateTenant(tenant, migrationPath), the adapter reads all .sql files in the provided directory, sorts them alphabetically, switches the PostgreSQL connection to the correct tenant (setting search_path if needed), and executes the raw SQL.

Note: The adapter executes the SQL blindly; it does not currently maintain a migrations_history table natively to track state. The consuming application must ensure migrations are idempotent (e.g., CREATE TABLE IF NOT EXISTS) or manage state tracking manually in the seedFn.

6. Environment Variables

tenant-postgres itself reads no environment variables directly. Your application uses them to initialize the pg Pools that you pass to the adapter.

VariableRequiredExamplePurposeUsed By
PG_ADMIN_URLYespostgres://user:pass@localhost:5432/mainConnection string for the control-plane/admin pool.App (pg.Pool)
PG_TENANT_BASE_URLOptionalpostgres://user:pass@localhost:5432Base connection string to generate DB-per-tenant URLs.App (getTenantPool)

7. Running Locally

Setting up local development:

1. Start PostgreSQL & Create Admin Database

Ensure Postgres is running and create an initial database.

bash
createdb my_admin_db

2. Install Dependencies

Install the tenant adapter and the postgres driver.

bash
npm install @node-tenant/tenant-postgres pg

3. Create Migration File

Create a test SQL migration file at ./migrations/001_init.sql.

sql
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL
);

4. Provision and Query a Tenant

Write a quick Node script to test the adapter.

typescript
import { Pool } from 'pg';
import { createPostgresDatabaseAdapter } from '@node-tenant/tenant-postgres';

const adminPool = new Pool({ connectionString: 'postgres://localhost:5432/my_admin_db' });
const adapter = createPostgresDatabaseAdapter({ adminPool });
const tenant = { id: 1, slug: 'test_tenant', strategy: 'schema' };

async function run() {
  await adapter.createTenantDatabase(tenant);
  await adapter.migrateTenant(tenant, './migrations');
  
  const client = await adapter.switchTenant(tenant);
  await client.query("INSERT INTO users (name) VALUES ('Alice')");
  
  const res = await client.query("SELECT * FROM users");
  console.log("Tenant Users:", res.rows);
  
  client.release();
}
run();

8. Deployment Guide

Note: PM2, PgBouncer, Nginx, Docker, and SSL are deployment/infrastructure tools used by the consuming application. tenant-postgres does not automatically configure or manage them.

PostgreSQL multi-tenancy heavily relies on connection limits. Keep these in mind for production:

  1. Connection Limits: A default Postgres server usually limits connections to 100. If you use the database strategy for 50 tenants, and each creates a pool of 10 connections, your app will crash. You must use an external connection pooler like PgBouncer if adopting the database strategy at scale.
  2. Schema Strategy Performance: The schema strategy is highly efficient for Postgres connections because all tenants share the single adminPool. SET search_path happens on the checked-out connection and costs virtually zero overhead.
  3. Releasing Clients: You must always call client.release() after using the client returned by switchTenant(tenant), or your application will leak connections.

9. DNS Configuration

Note: tenant-postgres operates entirely at the PostgreSQL SQL layer. Hostname parsing and tenant-to-subdomain resolution must be implemented by the consuming HTTP application.

For wildcard subdomains (*.example.com), configure a CNAME or A Record. When the request reaches your Node.js app, parse the subdomain (e.g. acme) and use it to look up the Tenant object. Finally, pass that object to adapter.switchTenant(tenant) to retrieve the correctly scoped PostgreSQL client.

10. Verifying the Deployment

Verify your production databases using `psql`.

1. Verify Database Strategy Creation

Check if databases were physically created.

sql
SELECT datname FROM pg_database WHERE datname LIKE 'tenant_%';

2. Verify Schema Strategy Creation

Connect to the admin database and list schemas.

sql
\dn

3. Verify search_path

When running your Node.js app, you can log the active path.

typescript
const client = await adapter.switchTenant(tenant);
const res = await client.query("SHOW search_path");
console.log(res.rows[0]); // Should print the tenant slug

11. Common Problems

SymptomCauseSolution
Connection pool exhaustion / TimeoutFailing to call client.release() after queries finish.Always wrap query logic in try/finally blocks and call client.release().
Permission denied to create databaseThe Postgres user associated with adminPool lacks the CREATEDB role.Run ALTER USER my_user CREATEDB; via a superuser account.
"schema already exists" during migrationsMigration .sql files are not idempotent.Use CREATE TABLE IF NOT EXISTS in raw SQL migration files.
migrateAll throws an errorThe adapter intentionally blocks this method.Iterate over your tenants array via your Control Plane storage, and call migrateTenant in a loop.

12. Best Practices

Database Architecture

  • Strategy Choice: Default to the schema strategy for typical SaaS apps to share connection pools efficiently. Use the database strategy only if strict physical compliance separation is mandated.
  • Template Database: For the database strategy, create a custom template_tenant database with pre-installed extensions (like uuid-ossp) and configure the adapter to use it via options.template.

Security & Isolation

  • search_path Injection: The adapter wraps the tenant slug in double quotes (SET search_path TO "slug"), preventing basic SQL injection, but validate slugs rigorously on tenant creation.
  • Idempotent Migrations: Because this adapter uses raw SQL execution, structure all migrations carefully.

13. Internal Workflow

The following diagram illustrates the internal PostgreSQL request lifecycle implemented by this adapter:

mermaid
flowchart TD
    A[Client Request] --> B[App resolves Tenant Object]
    B --> C["dbAdapter.switchTenant(tenant)"]
    C --> D{tenant.strategy}
    D -- "database" --> E["getTenantPool(slug)"]
    D -- "schema" --> F["adminPool"]
    E --> G[pool.connect()]
    F --> G
    G --> H{tenant.strategy}
    H -- "schema" --> I["client.query('SET search_path TO slug')"]
    H -- "database" --> J[No operation required]
    I --> K[Return PoolClient]
    J --> K
    K --> L[App executes SQL Query]
    L --> M["client.release()"]

Explanation: The adapter handles the complexity of obtaining the correct connection. If the tenant uses the schema strategy, it pulls a connection from the main pool and modifies its search path dynamically. If it's a database strategy, it asks the app for the custom pool and uses it.

14. FAQs

Does every tenant have a separate PostgreSQL database or schema?

It depends entirely on the strategy property of the Tenant object. You can even mix them in the same application!

Does it provide migrations?

Yes, natively! Passing a folder of .sql files to migrateTenant will sequentially execute them in the tenant's isolated context.

How should a tenant be deleted?

Call deleteTenantDatabase(tenant). For the database strategy, it safely terminates active connections (pg_terminate_backend) before dropping the database.

Does it configure PM2, PgBouncer, or Nginx?

No. It operates strictly at the application SQL driver layer.

15. Deployment Checklist

Copy this checklist into your issue tracker before going to production:

markdown
- [ ] Node.js version verified
- [ ] Dependencies installed (pg)
- [ ] Production environment variables configured
- [ ] PostgreSQL production server configured
- [ ] PostgreSQL roles/permissions verified (CREATEDB if using 'database' strategy)
- [ ] Connection pool configuration verified (PgBouncer evaluated for 'database' strategy)
- [ ] Migration files (.sql) are idempotent
- [ ] Tenant creation tested
- [ ] Tenant isolation (schema path or db) tested
- [ ] Application build completed
- [ ] PM2 configured
- [ ] Reverse proxy configured
- [ ] SSL configured
- [ ] DNS configured
- [ ] Wildcard DNS configured if required
- [ ] APIs verified
- [ ] PostgreSQL queries correctly releasing clients (client.release())
- [ ] Logs verified
- [ ] Backups configured
- [ ] Restore process tested