Design a Postgres schema that enforces tenant isolation in the database itself and stays fast as data grows.
Recommended stack
- Postgres 16+ - database. Constraints, RLS, generated columns, and pgvector cover almost every app need.
- Row-Level Security - tenant isolation. Policies make 'whose row is this' a database guarantee, not an app convention.
- pgvector - embeddings. Vector search next to relational data; HNSW indexes for scale.
- SQL migrations (versioned) - change control. Reviewable, ordered, reversible-by-forward-fix schema history.
Build steps
- Every tenant-owned table gets org_id; enable RLS deny-by-default, then write explicit policies per verb.
- Wrap membership checks in
security definerhelper functions to avoid policy recursion and keep policies one-line. - Constrain everything: foreign keys, checks on enums, unique on natural keys, not-null by default.
- Index for queries you actually run: composite (org_id, created_at desc) beats five single-column guesses.
- Test isolation with two JWTs in SQL: user A literally cannot select user B's rows.
Watch out for
- RLS policies that subquery the same table (infinite recursion) - use definer helpers.
- Forgetting service-role paths bypass RLS; guard privileged endpoints in code too.
- JSONB for data you filter on; columns are for queries, jsonb is for payloads.
Definition of done
- Cross-tenant read attempt returns zero rows with a member JWT
- EXPLAIN shows index scans on the top five queries
- Schema rebuilds from migrations alone on a blank database