Tenant isolation (RLS)
Every Wisteria workspace is a separate tenant. Data from one workspace must never be visible to another, even by accident — even via a developer mistake. The primary mechanism that enforces this is Postgres row-level security (RLS).
The model
Wisteria’s database is shared across all workspaces. There’s one users table, one courses table, one modules table — and so on. Every row has a company_id column (the workspace identifier).
RLS is a Postgres feature that filters rows automatically based on policies attached to each table. When a user issues a query, the database adds the relevant policy’s WHERE clause to it transparently. The user sees only rows matching the policy.
In Wisteria’s case, the policy is roughly: “You can only see rows where company_id matches your own workspace’s company_id.” Enforced for SELECT, INSERT, UPDATE, and DELETE.
Why we use RLS (instead of application-level filters)
Application-level filtering — “every query should include a WHERE company_id = X clause” — works when every developer remembers to write it. RLS works even when developers forget.
The risk of relying on app-level filtering: one missed WHERE clause in one query, one careless join, one bug — and one workspace’s data leaks to another. With RLS, the database itself adds the clause; the developer can’t forget.
How it composes with our backend
Wisteria’s backend uses Supabase. When a request comes in:
- The user’s JWT token identifies them.
- The backend passes the token to Supabase.
- Supabase resolves the token to a
user_id. - RLS policies use that
user_idto look up the user’scompany_id(the workspace). - Every query then filters by that
company_id.
This is all server-side. The frontend never sees raw data outside the tenant; the backend never sees raw data outside the tenant.
Helper functions
Common policy logic lives in SECURITY DEFINER Postgres functions:
get_my_company_id()— returns the current user’s workspaceget_my_role()— returns the current user’s rolecan_see_course(course_id)— composite check for content distributionis_org_admin_for(target_company_id)— for cross-workspace org governance
These are battle-tested in Wisteria’s codebase. New tables get policies that compose existing helpers rather than inlining the same checks every time.
What this means for customers
- Your data is invisible to other Wisteria customers — at the database level, not just the app level.
- A bug in one developer’s code can’t expose your data to a different tenant. RLS gives even buggy code a safety net.
- Wisteria employees with database access can see your data. RLS protects tenant-vs-tenant; it doesn’t protect customer-vs-Wisteria. See Token storage per provider for our handling of access tokens specifically.
Service-role bypass
Postgres has a “service role” that bypasses RLS — Wisteria uses it for specific server-side operations (user creation, audit log writes, cross-workspace cron jobs). Service-role usage is:
- Limited to specific routes — an allowlist enforced by linting
- Auditable — every service-role call is logged
- Justified per route — each route using the bypass has a documented reason
The roadmap is to drive service-role usage to zero except for the smallest unavoidable surface.
Storage isolation
File storage (cover images, certificate templates, integration files) uses Supabase Storage. Buckets have RLS-equivalent policies that scope access by workspace.
module-covers— public bucket; URLs are unguessablemodule-sources— private bucket; only the workspace owners can list/readcertificate-templates— private; per-workspace access only
What’s NOT in RLS
A few categories Wisteria doesn’t currently enforce via RLS:
- Anthropic / OpenAI API calls — when Wisteria sends data to Claude or Whisper, that’s a network call out of the database, so RLS doesn’t apply. The data goes to the AI provider scoped to the tenant making the call. See PII & encryption at rest.
- Static assets in the public directory — fonts, logos, the Wisteria mark. Public on purpose.
- Audit log writes — done via service role for performance. The reads are RLS-gated (audit logs are visible only to your own workspace’s super_admin + auditor).
Verification
Customers occasionally ask: “How do I verify my data is isolated?”
Three signals:
- The RLS policy itself. Available on request — every tenant-scoped table has its policy text published.
- A penetration test. We have an annual third-party pentest report; available under NDA for customer review.
- The audit log of cross-tenant attempts. Any service-role call that touches multiple tenants is logged; the log is auditable.
What if you suspect a leak
Email security@getwisteria.com. We treat this as the highest-priority alert.
The contact can be used for any kind of suspected breach or unexpected access — credentials, API surface, data, anything.