Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ency07/B2B-import/llms.txt

Use this file to discover all available pages before exploring further.

Row-Level Security (RLS) is PostgreSQL’s mechanism for attaching authorization logic directly to tables. When a query runs, the database engine automatically appends the active policy’s USING clause as a WHERE predicate, making it impossible — at the database layer — for a user to read or write rows outside their permitted scope. In B2B Import ERP, RLS is the last line of tenant isolation defence after application-level checks.
The supabaseAdmin service-role client used by all Server Actions bypasses all RLS policies. This is intentional — Server Actions run on trusted server infrastructure and need to perform cross-tenant operations (e.g., the wizard creates clients before a session exists). Never expose the service-role key to the browser or bundle it into client-side code.

How RLS Works with Supabase

When a user authenticates, Supabase issues a signed JWT containing the user’s auth.uid() (the UUID from auth.users). Every database query made by the supabase anon/user client sends this JWT in the Authorization header. PostgreSQL extracts it via the auth.uid() built-in function, making it available to policy expressions. The users table joins auth.uid() to the operational tenant_id and is_platform_user flag:
-- Resolve the operational tenant_id for any authenticated user
SELECT tenant_id FROM users WHERE auth_user_id = auth.uid();

-- Check for platform super-admin
SELECT is_platform_user FROM users WHERE auth_user_id = auth.uid();
These subqueries appear inside every RLS policy and are automatically inlined by the query planner.

Standard Tenant Isolation Policy

Every operational table uses a variant of this template, as seen in 20260617000000_init_core.sql:
CREATE POLICY sites_isolation ON sites
  FOR ALL USING (
    is_platform_super_admin()
    OR tenant_id = (SELECT tenant_id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
  );
The is_platform_super_admin() helper function (defined in the same migration) encapsulates the is_platform_user check:
CREATE OR REPLACE FUNCTION is_platform_super_admin()
RETURNS boolean AS $$
BEGIN
    IF session_user = 'postgres' THEN
        RETURN true;
    END IF;
    RETURN COALESCE(
        (SELECT is_platform_user FROM users WHERE auth_user_id = auth.uid() LIMIT 1),
        false
    );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
This FOR ALL policy covers SELECT, INSERT, UPDATE, and DELETE with a single clause. Tables that need more granular control define separate per-operation policies.

SUPER_ADMIN Bypass

Users whose users.is_platform_user = true pass the first condition of every isolation policy and therefore see all tenants’ data. This flag is set only for platform-level administrators who manage the multi-tenant infrastructure — it is never granted to individual tenant users. is_platform_super_admin() is also used to gate write access on tables like permissions:
CREATE POLICY permissions_write_admin ON permissions
  FOR ALL TO authenticated
  USING (is_platform_super_admin());
And as a full bypass on user_access_logs:
CREATE POLICY user_access_logs_super_admin ON user_access_logs
  FOR ALL TO authenticated
  USING (is_platform_super_admin())
  WITH CHECK (is_platform_super_admin());

Read-Only Policies: AUDITOR Role

The AUDITOR role is granted SELECT on operational and audit tables but cannot INSERT, UPDATE, or DELETE. This is enforced by defining separate operation-scoped policies:
-- AUDITOR and GERENTE can read access logs within their tenant
CREATE POLICY user_access_logs_tenant_auditor ON user_access_logs
  FOR SELECT TO authenticated
  USING (
    tenant_id = (SELECT tenant_id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
    AND EXISTS (
      SELECT 1 FROM user_roles ur
      JOIN roles r ON ur.role_id = r.id
      WHERE ur.user_id = (SELECT id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
        AND r.role_code IN ('AUDITOR', 'GERENTE')
    )
  );
The FOR SELECT scope means this policy never allows writes — the absence of a write policy for the AUDITOR role implicitly denies mutations.

Client Portal Policies

The CLIENTE role is used for the self-service client portal. Portal users are linked to a specific client_id through their users record. Their policies restrict access to rows where client_id or tenant_id matches their own scope. A typical read-only portal policy follows the same tenant-isolation pattern but scoped to the client_id found on the authenticated user’s record:
-- Example pattern: client sees only their own invoices
CREATE POLICY portal_client_invoices ON invoices
  FOR SELECT TO authenticated
  USING (
    tenant_id = (SELECT tenant_id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
    AND client_id = (SELECT id FROM clients WHERE assigned_user_id =
                     (SELECT id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
                     LIMIT 1)
  );
Portal users have no INSERT or DELETE policies on invoices or quotes — they are read-only consumers of the ERP data.

User Self-Service Policies

Regular authenticated users can read their own access log entries regardless of role:
CREATE POLICY user_access_logs_own_records ON user_access_logs
  FOR SELECT TO authenticated
  USING (
    user_id = (SELECT id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
  );
For INSERT (e.g., logging their own login event):
CREATE POLICY user_access_logs_insert_authenticated ON user_access_logs
  FOR INSERT TO authenticated
  WITH CHECK (
    tenant_id = (SELECT tenant_id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
    AND user_id = (SELECT id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
  );

audit_log Policy

audit_log is driven entirely by server-side triggers — the application never calls INSERT on it directly. The core policy (from 20260617000000_init_core.sql) uses the standard FOR ALL tenant-isolation pattern:
CREATE POLICY audit_log_isolation ON audit_log
  FOR ALL USING (
    is_platform_super_admin()
    OR tenant_id = (SELECT tenant_id FROM users WHERE auth_user_id = auth.uid() LIMIT 1)
  );
This means:
  • SELECT: Any authenticated user within the same tenant can read their own audit trail records.
  • INSERT: Permitted for authenticated users within the same tenant (triggers fire under their session context).
  • UPDATE / DELETE: No additional policies exist — effectively denied for all non-SUPER_ADMIN roles because application Server Actions never issue these statements on audit_log.

Service Role Bypass

supabaseAdmin is constructed with the SUPABASE_SERVICE_ROLE_KEY environment variable. The Supabase gateway recognises service-role tokens and instructs PostgreSQL to execute queries as a superuser, bypassing all RLS policies entirely. Intentional uses of the service role in this project:
Use CaseWhy Service Role is Needed
Wizard form submissionVisitor is unauthenticated — no JWT, no tenant session.
Server Actions (all)Trusted server code; avoids per-user policy overhead.
Admin operations (tenant creation, seeding)Require cross-tenant writes.
The SUPABASE_SERVICE_ROLE_KEY must only ever appear in server-side environment variables (.env.local, Vercel environment variables without the NEXT_PUBLIC_ prefix). If it is accidentally bundled into client JavaScript it grants any browser user full database access without any RLS enforcement.

Security Testing

Run the built-in security audit script to verify that RLS policies are active on all expected tables and that no policy gaps exist:
npm run test:security-audit
This script connects to the Supabase project and asserts that rowsecurity = true for every table listed in the security model and that no table has zero policies defined.
You can also inspect RLS status directly in the Supabase dashboard under Table Editor → [table] → RLS or via the SQL Editor:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;

Build docs developers (and LLMs) love