Tenant isolation, in detail.
UniRubric uses shared infrastructure with database-enforced per-tenant isolation. This is the same model used by every modern multi-tenant SaaS — Slack, Linear, Vercel itself — but here is how it works in our environment specifically.
Layer 1 — Row-Level Security at the database
Every tenant-owned table in the production Supabase database has at least one RLS policy. The policy restricts SELECT, INSERT, UPDATE, and DELETE to rows whose org_id matches the authenticated caller’s claimed tenant, or whose user_id matches the authenticated caller’s auth.uid() for B2C accounts.
Policies are enforced inside Postgres, not in application code. A bug in application code that forgot a WHERE clause cannot leak data across tenants — Postgres will still return only rows the policy permits. This is the defence-in-depth line that survives application-layer mistakes.
Layer 2 — Service-role segregation
The Supabase service role can bypass RLS. We treat it as a production-only credential held in three classes, each in a different Vercel environment variable, never shipped to the browser, never logged:
- worker — used by the QStash grading worker to read submissions and write grading_runs across tenants as needed for queued jobs.
- admin — used by maintenance scripts and manual support actions. Every use is audit-logged.
- public-api — used by the public REST API after the application-layer auth check passes, scoped to the request’s tenant.
Service-role keys are rotated on the schedule documented in master spec §23. A compromised key triggers immediate rotation and a tenant-wide audit log review for unexpected activity.
Layer 3 — Cross-tenant code paths are explicit
There is no general-purpose “list everything” endpoint that crosses tenants. The only routes that look at data outside the caller’s tenant are:
- The QStash grading worker, which loads the specific submission and rubric named in the queue payload.
- The LTI launch handler, which resolves a platform-issued deployment ID to its UniRubric org.
- Admin-only routes under
/admin/*that are gated by both authentication and role checks.
Each of these is a small, named code path with explicit tenant resolution. There is no transitive trust — a code path cannot “promote” a tenant claim from one request to another.
Layer 4 — Audit, not just prevention
Every privileged action — approve, override, delete, role change — writes a row to the audit_log table at insertion time. Audit rows are immutable from application code; only the database superuser can rewrite them, and the database superuser is held by Supabase and used only for the platform-level operations they expose to us.
Verification
The RLS policies are version-controlled in supabase/migrations/*.sql and reviewed at every schema change. The Supabase database advisor runs continuously and flags any table with RLS enabled but no policies, or any function with overly permissive grants. We treat advisor WARN+ as a deploy blocker.
On request, we provide tenant-scoped audit exports covering the last 90 days — every read by an admin, every cross-tenant code path entry that touched the requesting tenant’s rows.