Connection Management
PostgreSQL connection pool configuration, import paths, and the Database/Transaction type exports.
The @repo/db package manages a single connection pool shared across the entire Node.js process. This page covers how the pool is configured, the import paths that control access, and the exported types.
Connection Pool
// packages/db/src/client.ts
const pool = new Pool({
connectionString: process.env["DATABASE_URL"],
max: 5,
});
export const db = drizzle(pool, { schema });
The pool uses node-postgres (pg.Pool) with a maximum of 5 connections. The DATABASE_URL is read from the environment:
| Environment | URL | Source |
|---|---|---|
| Local dev | postgresql://trovella:trovella_dev@localhost:5433/trovella | .env file |
| CI | Same format, Docker service container on port 5433 | GitHub Actions env |
| Production | postgresql://trovella-app:<pw>@<ip>:5432/trovella | Secret Manager |
The Drizzle instance is created with the full schema import so that the relational query API (db.query.*) works.
Import Paths
The package uses two export paths to enforce the tenant isolation rule:
@repo/db (default)
import { withTenantContext, checkDatabase } from "@repo/db";
import type { Database, Transaction } from "@repo/db";
This is the standard import for application code. It exports:
| Export | Purpose |
|---|---|
withTenantContext | Tenant-scoped transaction wrapper |
checkDatabase | Health check (SELECT 1) |
createPAT, listUserPATs, revokePAT | User-scoped PAT operations (encapsulate bare db internally) |
Database (type) | Type of the Drizzle instance |
Transaction (type) | Type of a Drizzle transaction |
| All schema exports | Tables, relations, enums |
The bare db is not exported from this path.
@repo/db/client
import { db } from "@repo/db/client";
This exports the raw Drizzle instance that connects as the superuser. Importing from this path is a signal that the code intentionally bypasses RLS. Legitimate uses:
- Migration runner (
packages/db/src/migrate.ts) - Seed scripts (
packages/db/src/seed.ts) - Auth bootstrap (Better Auth internals)
- Health checks (already wrapped by
checkDatabase) - PAT queries (user-scoped, not tenant-scoped -- encapsulated in
@repo/db)
@repo/db/schema
import { researchPlan, planStep, member } from "@repo/db/schema";
Pure schema definitions -- table objects, relation definitions, enum types. No runtime database access. Useful for type imports and building queries.
Type Exports
// packages/db/src/client.ts
export type Database = typeof db;
export type Transaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
Use Database as the parameter type when writing helper functions that accept a database connection or transaction:
import type { Database } from "@repo/db";
export async function handleCreateResearchPlan(
tx: Database,
params: CreateResearchPlanParams,
organizationId: string,
userId: string,
): Promise<{ planId: string; stepIds: string[] }> {
// tx works for both db and transaction objects
}
The Transaction type is the raw Drizzle transaction type, which is a subset of Database. In practice, Database is used everywhere because withTenantContext casts the transaction to typeof db so the full API is available.
Health Check
import { checkDatabase } from "@repo/db";
const { ok, latencyMs } = await checkDatabase();
checkDatabase runs SELECT 1 against the pool and returns the result with timing. It uses the bare db internally (no tenant context needed for a connectivity check).
Environment Variable Loading
Scripts in packages/db/ (migrate, seed, generate) load the root .env via tsx --env-file=../../apps/web/.env. The Next.js application loads .env automatically. Production environments inject DATABASE_URL via Secret Manager at deploy time.