Skip to content

@stratum-hq/db-adapters

@stratum-hq/db-adapters provides PostgreSQL adapters that automatically scope queries to the current tenant using Row-Level Security. It supports raw pg, Prisma, and Sequelize, plus helpers for enabling RLS on your tables.

Installation

Terminal window
npm install @stratum-hq/db-adapters @stratum-hq/core pg

Raw PostgreSQL Adapter

For applications using pg (node-postgres) directly:

import { Pool } from "pg";
import { RawAdapter, createTenantPool } from "@stratum-hq/db-adapters";
import { getTenantContext } from "@stratum-hq/sdk";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Option 1: Automatic context from AsyncLocalStorage
const tenantPool = createTenantPool(pool, () => getTenantContext().tenant_id);
const result = await tenantPool.query("SELECT * FROM orders");
// Option 2: Manual adapter usage
const adapter = new RawAdapter(pool);
const result = await adapter.query("tenant-id", "SELECT * FROM orders");

How It Works

The adapter wraps every query in a transaction that:

  1. Calls BEGIN
  2. Sets app.current_tenant_id via parameterized set_config() — fully SQL-injection-safe
  3. Executes your query (automatically filtered by RLS policies)
  4. Calls COMMIT
  5. Resets the tenant context and releases the connection

Prisma Adapter

For a full tutorial with Docker setup, RLS policies, and Express integration, see the Prisma + PostgreSQL RLS guide.

For applications using Prisma ORM:

import { PrismaClient } from "@prisma/client";
import { Pool } from "pg";
import { withTenant } from "@stratum-hq/db-adapters";
import { getTenantContext } from "@stratum-hq/sdk";
const prisma = new PrismaClient();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const tenantPrisma = withTenant(
prisma,
() => getTenantContext().tenant_id,
pool,
);
// All queries automatically scoped to the current tenant
const orders = await tenantPrisma.order.findMany();

RLS Management

Enable RLS on a Table

import { enableRLS, createPolicy } from "@stratum-hq/db-adapters";
const client = await pool.connect();
try {
await enableRLS(client, "orders"); // ENABLE + FORCE ROW LEVEL SECURITY
await createPolicy(client, "orders"); // CREATE POLICY tenant_isolation
} finally {
client.release();
}

Disable RLS

import { dropPolicy, disableRLS } from "@stratum-hq/db-adapters";
await dropPolicy(client, "orders");
await disableRLS(client, "orders");

Check RLS Status

import { isRLSEnabled } from "@stratum-hq/db-adapters";
const enabled = await isRLSEnabled(client, "orders"); // true/false

Migration Helpers

One-step table migration that adds tenant_id, enables RLS, and creates the isolation policy:

import { migrateTable } from "@stratum-hq/db-adapters";
const client = await pool.connect();
try {
await client.query("BEGIN");
await migrateTable(client, "orders");
await client.query("COMMIT");
} finally {
client.release();
}

Or step-by-step for more control:

import {
addTenantColumn,
enableRLS,
createIsolationPolicy,
} from "@stratum-hq/db-adapters";
await addTenantColumn(client, "orders"); // ALTER TABLE ADD tenant_id UUID NOT NULL
await enableRLS(client, "orders"); // ENABLE + FORCE ROW LEVEL SECURITY
await createIsolationPolicy(client, "orders"); // CREATE POLICY tenant_isolation

Session Context

Low-level functions for managing the PostgreSQL session variable:

import {
setTenantContext,
resetTenantContext,
getCurrentTenantId,
} from "@stratum-hq/db-adapters";
await setTenantContext(client, "tenant-uuid");
const id = await getCurrentTenantId(client); // "tenant-uuid"
await resetTenantContext(client); // RESET app.current_tenant_id

Drizzle Adapter

For a full tutorial with Docker setup, RLS policies, and Express integration, see the Drizzle ORM multi-tenancy guide.

For applications using Drizzle ORM:

import { Pool } from "pg";
import { drizzle } from "drizzle-orm/node-postgres";
import { withDrizzleTenant } from "@stratum-hq/db-adapters";
import { getTenantContext } from "@stratum-hq/sdk";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const db = drizzle(pool);
const tenantDb = withDrizzleTenant(
db,
() => getTenantContext().tenant_id,
pool,
);
// All queries automatically scoped to the current tenant
const result = await tenantDb.execute(db.select().from(orders));

Sequelize Adapter

For applications using Sequelize ORM:

import { Pool } from "pg";
import { SequelizeAdapter, withTenantScope } from "@stratum-hq/db-adapters";
import { getTenantContext } from "@stratum-hq/sdk";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Option 1: Convenience function — wraps every query in a tenant-scoped transaction
const tenantSequelize = withTenantScope(
sequelize,
() => getTenantContext().tenant_id,
pool,
);
const orders = await tenantSequelize.query("SELECT * FROM orders");
// Option 2: Manual adapter usage
const adapter = new SequelizeAdapter(pool);
const result = await adapter.executeWithTenantContext("tenant-id", async (client) => {
return client.query("SELECT * FROM orders");
});

How It Works

The Sequelize adapter wraps every query() call in a Sequelize transaction that:

  1. Calls sequelize.transaction()
  2. Sets app.current_tenant_id via parameterized set_config() inside the transaction
  3. Executes your query on the same connection (guaranteed by the transaction)
  4. Commits the transaction

This ensures the set_config and your query always execute on the same database connection — preventing tenant context leaks in connection-pooled environments.

Security

PropertyImplementation
Table name validationAll DDL operations validate against /^[a-zA-Z_][a-zA-Z0-9_]*$/
Parameterized contextTenant ID set via set_config($1, true) — fully parameterized
FORCE RLSenableRLS() always enables FORCE, preventing bypass by table owners
Connection cleanupAlways reset tenant context when releasing connections