Database architecture skills, docs, and rules for high-demand multi-tenant commerce platforms (PostgreSQL source of truth, Neo4j as derived GraphRAG projection, transactional outbox, RLS-based tenant isolation). Includes live schema introspection workflow via explicit Supabase MCP/read-only schema sources.
77
97%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Tenant isolation via PostgreSQL RLS. Skills cite this file as the canonical implementation. Copy as-is for new tenant-scoped tables.
The application sets the tenant context once per request, immediately after authentication and before any business query:
SET LOCAL app.tenant_id = '42';
SET LOCAL app.user_id = '123';
SET LOCAL app.actor_role = 'tenant_user'; -- 'tenant_user' | 'tenant_admin' | 'platform_admin'SET LOCAL is critical — the value is scoped to the current transaction
and cannot leak across requests via connection pooling.
The application uses a connection-pool-aware wrapper that injects this SET into every request transaction. Never issue queries without it.
Apply this template to every tenant-scoped table:
ALTER TABLE catalog_products ENABLE ROW LEVEL SECURITY;
ALTER TABLE catalog_products FORCE ROW LEVEL SECURITY;
-- FORCE is critical: without it, the table owner bypasses RLS
CREATE POLICY catalog_products_tenant_isolation
ON catalog_products
USING (tenant_id = current_setting('app.tenant_id')::bigint)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);USING controls what's visible to SELECT/UPDATE/DELETE.
WITH CHECK controls what INSERT/UPDATE rows are allowed to have.
Both are required — omitting WITH CHECK allows tenant ID forgery on insert.
Some operations (cross-tenant analytics, support tooling) need cross-tenant access. Carve out a separate role with explicit, audited bypass:
CREATE ROLE platform_admin NOINHERIT;
CREATE POLICY catalog_products_platform_admin_bypass
ON catalog_products
AS PERMISSIVE
FOR SELECT
TO platform_admin
USING (true);The application middleware logs every platform_admin query separately and requires SSO + step-up auth.
When tenant users have varying permissions inside their own tenant:
CREATE POLICY catalog_products_tenant_user_select
ON catalog_products
FOR SELECT
USING (
tenant_id = current_setting('app.tenant_id')::bigint
AND (
current_setting('app.actor_role') = 'tenant_admin'
OR EXISTS (
SELECT 1
FROM identity_role_grants g
WHERE g.tenant_id = current_setting('app.tenant_id')::bigint
AND g.user_id = current_setting('app.user_id')::bigint
AND g.permission = 'catalog.read'
)
)
);Composite indexes must support these queries. Add a covering index on
identity_role_grants (tenant_id, user_id, permission).
Soft-deleted rows should not appear in default queries but should remain visible to admins for audit. Combine RLS with a partial unique index:
CREATE UNIQUE INDEX catalog_products_active_sku_key
ON catalog_products (tenant_id, sku)
WHERE deleted_at IS NULL;
CREATE POLICY catalog_products_hide_deleted
ON catalog_products
FOR SELECT
USING (
deleted_at IS NULL
OR current_setting('app.actor_role') IN ('tenant_admin', 'platform_admin')
);Two policies on the same table are AND-combined for the SELECT command, so
both tenant_isolation and hide_deleted apply.
Composite FKs that include tenant_id make cross-tenant references
impossible at the database level:
ALTER TABLE sales_order_items
ADD CONSTRAINT sales_order_items_product_fk
FOREIGN KEY (tenant_id, product_id)
REFERENCES catalog_products (tenant_id, id)
ON DELETE RESTRICT;This requires catalog_products to have (tenant_id, id) as a UNIQUE
constraint:
ALTER TABLE catalog_products
ADD CONSTRAINT catalog_products_tenant_id_key UNIQUE (tenant_id, id);The composite unique is already implied by the PK + the tenant_id
column, but PostgreSQL needs an explicit UNIQUE for FK targeting.
Mandatory tests for every tenant-scoped table:
-- Setup: two tenants, each with one product
INSERT INTO tenancy_tenants (id, name) VALUES (1, 'A'), (2, 'B');
INSERT INTO catalog_products (tenant_id, sku, name) VALUES
(1, 'SKU-1', 'A product'),
(2, 'SKU-2', 'B product');
-- Test 1: tenant 1 sees only its product
SET LOCAL app.tenant_id = '1';
SELECT count(*) FROM catalog_products; -- expect 1
-- Test 2: tenant 1 cannot insert as tenant 2
SET LOCAL app.tenant_id = '1';
INSERT INTO catalog_products (tenant_id, sku, name) VALUES (2, 'X', 'Y');
-- expect: ERROR — new row violates row-level security policy
-- Test 3: tenant 1 cannot update tenant 2's product (UPDATE returns 0)
SET LOCAL app.tenant_id = '1';
UPDATE catalog_products SET name = 'hacked' WHERE id = (
SELECT id FROM catalog_products WHERE sku = 'SKU-2' -- visible to admin only
);
-- Test 4: missing tenant context fails closed
RESET app.tenant_id;
SELECT count(*) FROM catalog_products;
-- expect: ERROR — current_setting('app.tenant_id') returns NULLIf the production DB role bypasses RLS via superuser or BYPASSRLS, the
policies are silently ignored. Always run RLS tests as a non-bypass role.
FORCE ROW LEVEL SECURITY. Without it, the table owner
bypasses policies — and the migration runner is usually the owner.WITH CHECK. Without it, INSERT can forge tenant_id.current_setting('app.tenant_id') once and reusing. Each
policy must cast at evaluation time.app.tenant_id outside SET LOCAL. Connection-pooled
sessions leak the tenant context across requests — catastrophic.docs
skills
adr-drafting
commerce-database-architecture
graph-rag-boundary-review
mermaid-diagram-review
outbox-and-eventing-design
postgres-schema-introspection
schema-evolution-workflow