Move tenant isolation into Postgres: wrap a request in runWithTenant() and every query issued inside — pooled or transactional — runs with the app.tenant_id GUC set, so RLS policies filter the rows application SQL forgot to. An unset GUC yields zero rows (fail-closed).
ALTERTABLE posts ENABLEROWLEVELSECURITY;
ALTERTABLE posts FORCEROWLEVELSECURITY;
CREATEPOLICY tenant_isolation ON posts
USING (tenant_id::text= ANY (string_to_array(NULLIF(current_setting('app.tenant_ids', true), ''), ',')));
Export
Description
runWithTenant(tenantId, fn)
Run fn with app.tenant_idandapp.tenant_ids set to one tenant
runWithTenantScope(tenantIds, fn)
Hierarchies (parent org over subsidiaries): app.tenant_ids holds every readable tenant, the first is also the acting app.tenant_id
runWithoutTenant(fn)
Explicitly leave a scope (platform jobs, the sweep that lists tenants)
DB_TENANT_ROLE — when it names a NOLOGIN role that owns nothing (e.g. app_user), every tenant-scoped statement runs under SET ROLE to it, so RLS applies even when the connection is the table owner or postgres under vk dev
RLS is skipped for table owners (without FORCE) and for BYPASSRLS roles — run the application as a role that owns nothing and keep migrations on a privileged one. @venturekit-pro/tenancy’s createTenantRlsMiddleware wires runWithTenant around each request.
Four Postgres-backed building blocks, each shipped with its own migration under the package’s vk.migrations directory (vk_data_001_idempotency.sql … vk_data_004_file_object.sql, auto-applied by vk migrate). Every helper accepts an optional querier / tableName so it can join an open transaction.
Durable, queryable background jobs — for work the product talks about (“which imports are still geocoding for this tenant”), where an SQS queue has no query surface. The claim is FOR UPDATE SKIP LOCKED, so several workers can drain the same table.
Metadata for uploaded objects — the record, where @venturekit/storage is the bytes. Deliberately uncoupled: sweepArchivedFiles takes object deletion as a port.
Feature flags and tenant settings over vk_feature_flags, vk_feature_flag_overrides and vk_tenant_settings (vk_data_005). Every function takes a Querier (query or tx.query). Flag keys match ^[a-z0-9][a-z0-9._-]{0,127}$.
Evaluation order, first match wins: user pin → tenant pin → global enabled → rollout (rolloutPercent > 0: the subject’s stable bucket) → off. The bucket hashes ${key}:${subject} (the user id when given, else the tenant id), so a subject sees the same answer on every request and across containers, and raising the percentage from 10 to 20 keeps the first 10 % on. Unknown keys evaluate to false (or your fallback), never an error.
Export
Description
createFlags({ querier, ttlMs? })
Flags — isEnabled(key, { tenantId?, userId? }, fallback?), evaluateAll(subject) (Record<key, boolean> to hydrate once per request), snapshot(), invalidate(). Keeps an in-memory snapshot of both tables refreshed lazily when older than ttlMs (default 30 000; 0 re-reads every call), so a flag check costs no query
The SQL-building half of “search my tables”, pure and unit-testable. Every fragment takes the $n parameter index you will bind, so it composes with the rest of your statement; identifiers are validated before interpolation.
Migration lines for a STORED generated, weighted tsvector column (sources: [{ column: 'title', weight: 'A' }, …], default column search_vector, config simple — no stemming, safe for mixed-language content) and its GIN index