A PostgreSQL connection pool reuses sessions across requests. A session-level tenant setting can therefore survive after one request releases its connection and affect the next borrower. Set the tenant locally inside a transaction, and run every protected query on that same checked-out client.
Last updated: October 1, 2026.
async function listTenantInvoices(pool, tenantId) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
"SELECT set_config('app.tenant_id', $1, true)",
[tenantId]
);
const result = await client.query(
'SELECT id, total FROM invoices ORDER BY id'
);
await client.query('COMMIT');
return result.rows;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}The third argument to set_config() is true, so the value is transaction-local. The transaction, tenant setting, and protected query all use one client rather than separate calls on the pool.
Make the policy fail closed
A policy can compare tenant_id with current_setting('app.tenant_id', true). The optional true returns null when the setting is absent, so an ordinary equality comparison matches no rows. PostgreSQL documents that set_config(..., true) lasts only for the current transaction.
Enable RLS and test through the same non-owner role used by the application. Superusers, roles with BYPASSRLS, and normally the table owner bypass policies; the row-security documentation explains these exceptions.
Test the pool, not only the policy
Run concurrent tests that alternate tenants through a small pool, omit the tenant context deliberately, and force both commit and rollback paths. Every missing-context query should return no protected rows. Always release the checked-out client in finally; a transaction abandoned on a pooled session is another cross-request hazard.
RLS is defense in depth, not a replacement for application scoping. Pair it with tenant-aware SQL, a deliberate multi-tenant database model, and preserved context in background jobs.