Row Level Security (RLS) is the second line of defense after application-layer tenant checking. Even if a bug in a Server Action bypassesDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ency07/B2B/llms.txt
Use this file to discover all available pages before exploring further.
validateTenantAccess(), the database itself will reject any query that violates the row’s policy. Every table in the public schema has ALTER TABLE ... ENABLE ROW LEVEL SECURITY applied. Policies are organized into two tiers by Postgres role: anon for the public site and authenticated for the ERP dashboard.
Two Policy Tiers
Tier 1 — Anonymous Policies (migration 20260621000038)
Migration 20260621000038_public_rls_policies.sql grants the minimum access the public site needs to function: read-only SELECT access to catalog, branding, and public content tables; INSERT access to the tables the lead-generation wizard writes to; and SELECT access on wizard tables where the action must read back a just-inserted row. No UPDATE or DELETE is ever granted to the anon role.
Tier 2 — Authenticated Policies (migration 20260622000039)
Migration 20260622000039_rls_authenticated_policies.sql restricts all table access for the authenticated role to rows that satisfy either of two conditions:
- The row’s
tenant_idmatches the tenant UUID embedded in the user’s JWT:tenant_id = (auth.jwt() ->> 'tenant_id')::uuid - The user is a platform super-admin:
is_platform_super_admin()
Tables with Anonymous Read / Write Policies
| Table | Anonymous Policy | Purpose |
|---|---|---|
product_categories | SELECT | Public catalog |
product_subcategories | SELECT | Public catalog |
product_families | SELECT | Public catalog |
product_series | SELECT | Public catalog |
products | SELECT (status = 'ACTIVO') | Public catalog |
product_specifications | SELECT | Product specs |
product_images | SELECT | Product media |
product_documents | SELECT | Product docs |
product_files | SELECT | CAD files |
media_assets | SELECT | All media |
seo_metadata | SELECT | Product SEO |
tenant_settings | SELECT (is_public = true) | Public branding |
website_pages | SELECT (status = 'ACTIVA') | Public web pages |
tenants | SELECT (status = 'Activo') | Wizard tenant validation |
leads | INSERT | Wizard lead capture |
clients | SELECT + INSERT | Wizard client upsert |
client_contacts | SELECT + INSERT | Wizard contact upsert |
diagnostic_reports | SELECT + INSERT | Wizard diagnostics |
wizard_sessions | INSERT | Wizard session tracking |
lead_sources | INSERT | Contact form submissions |
Authenticated Policy Example
The following policy from migration20260622000039 is representative of every authenticated policy in the system. The same USING and WITH CHECK expressions are applied to SELECT, INSERT, UPDATE, and DELETE via a single FOR ALL policy per table, plus an explicit FOR SELECT policy where separate read/write granularity is needed:
tenant_id is injected by Supabase Auth at sign-in time from the users table. This means a user can never access a row from a different tenant, even if they somehow obtain a valid access token for another tenant — the database will reject the query before it returns any data.
supabaseAdmin vs. getPublicServerClient()
| Client | Key | RLS applied? | Appropriate for |
|---|---|---|---|
getPublicServerClient() | SUPABASE_ANON_KEY | ✅ Yes | Wizard Server Actions, public catalog reads |
getSupabaseAdmin() | SUPABASE_SERVICE_ROLE_KEY | ❌ No (bypasses RLS) | Database migrations, seed scripts, platform admin operations |
getSupabaseAdmin() is initialized lazily — it throws Error: SUPABASE_SERVICE_ROLE_KEY no está configurada if the environment variable is absent. This prevents silent permission escalation in environments where the service key is intentionally not set (e.g. preview deployments).
Verifying RLS Coverage
After adding a new table or migration, verify that every table has RLS enabled and that all expected policies are present:check-rls-coverage.ts queries pg_policies and pg_tables to produce a report of any tables that have RLS disabled or that are missing the expected anon / authenticated policy pairs. Running this script is also a recommended step in the post-migration verification checklist (scripts/check-index-collisions.ts covers index health in the same pass).