Skip to Content
Wisteria is in beta — these docs are evolving fast.
SecurityTenant isolation (RLS)

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:

  1. The user’s JWT token identifies them.
  2. The backend passes the token to Supabase.
  3. Supabase resolves the token to a user_id.
  4. RLS policies use that user_id to look up the user’s company_id (the workspace).
  5. 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 workspace
  • get_my_role() — returns the current user’s role
  • can_see_course(course_id) — composite check for content distribution
  • is_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 unguessable
  • module-sources — private bucket; only the workspace owners can list/read
  • certificate-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:

  1. The RLS policy itself. Available on request — every tenant-scoped table has its policy text published.
  2. A penetration test. We have an annual third-party pentest report; available under NDA for customer review.
  3. 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.

Last updated on