The Architecture Overview

All tenants share the same Supabase database. Isolation is enforced by Row-Level Security (RLS) policies β€” PostgreSQL-level rules that filter every query based on the authenticated user's JWT claims.

When a user signs in, their JWT contains a workspace_id claim. Every RLS policy checks this claim against a workspace_id column in the table. Users literally cannot query data from other workspaces β€” the database enforces it, not the application code.

Setting Up the Workspace Structure

Tables you need:

1. workspaces (id, name, plan, created_at) 2. workspace_members (id, workspace_id, user_id, role, created_at) 3. All your business tables (with workspace_id column)

When a user signs up, create a workspace and a workspace_members record in a Supabase database function. The workspace_id goes into the user's JWT via a custom claim hook.

Writing RLS Policies

For every tenant-scoped table, enable RLS and add these policies:

SELECT policy:
CREATE POLICY "users can read own workspace data"
ON your_table FOR SELECT
USING (workspace_id = (auth.jwt() ->> 'workspace_id')::bigint);
INSERT policy:
CREATE POLICY "users can insert to own workspace"
ON your_table FOR INSERT
WITH CHECK (workspace_id = (auth.jwt() ->> 'workspace_id')::bigint);

This enforces isolation at the database level β€” no matter what your frontend or API does.

Role-Based Access Control

Within a workspace, different users have different permissions (owner, admin, member). Store this in workspace_members.role.

For data-level RBAC, use Supabase functions in your RLS policies:

CREATE FUNCTION get_user_role() RETURNS text AS $$
  SELECT role FROM workspace_members
  WHERE user_id = auth.uid()
  AND workspace_id = (auth.jwt() ->> 'workspace_id')::bigint
$$ LANGUAGE sql STABLE;

Then in policies: USING (get_user_role() IN ('admin', 'owner'))

Wiring It Up in WeWeb

In WeWeb, configure your Supabase data source with the authenticated user's token. WeWeb passes the JWT automatically with every request.

For your app's navigation and UI, read the role from a global variable (populated from the workspace_members table on login). Show/hide menu items, buttons, and entire sections based on role β€” this is presentation-only, the real security is in RLS.

Never hard-code tenant IDs in WeWeb. All data filtering should come from the authenticated user's JWT β€” Supabase handles the rest.