Home / RLS / owner_id vs workspace_id
Supabase RLS · Decision
owner_id vs workspace_id: which RLS pattern to use
Single-user data? Scope rows to owner_id = auth.uid(). Anything a team shares? Scope to workspace_id + a membership check. If teams are even plausible, start with workspace — retrofitting it later is the painful path.
01 The two models
Owner-scoped — every row belongs to one user. Simplest possible RLS: user_id = (select auth.uid()). Perfect for personal apps, single-player tools, per-user settings.
Workspace-scoped — rows belong to a team/workspace, and membership decides access. More moving parts (a members table, a membership function), but it's the only model that supports sharing, invites, and roles.
02 How to choose
- Will two people ever need to see the same row? If yes → workspace. If never → owner.
- Invites, seats, roles, org billing on the roadmap? → workspace, from day one.
- A genuinely personal tool (a journal, a solo dashboard)? → owner. Don't add workspace machinery you'll never use.
The trap is picking owner because it's easy, then bolting on teams a year later — every table, policy, and query has to change under load. If teams are plausible, the small upfront cost of workspace is cheap insurance.
03 Side by side
owner-scoped
create policy "select own" on notes for select using (user_id = (select auth.uid()));
workspace-scoped
create policy "members read" on notes for select using (workspace_id in (select public.user_workspace_ids()));
04 Migrating owner → workspace later
If you must retrofit: create a workspace per existing user, backfill workspace_id from user_id, add the membership rows (each user is the sole member of their own workspace), then swap the policies. It's doable, but it's a data migration plus a policy rewrite across every table — which is exactly why starting with workspace is worth considering.
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive — the same class of bug, caught on the pull request instead of in prod.
Or start from nextjs-supabase-starter — auth + a table with RLS + an isolation test, so a fresh table is safe by default.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Can I mix both in one app?
Yes, and many apps do — per-user settings stay owner-scoped while shared resources are workspace-scoped. Just be deliberate about which model each table uses, and keep the membership function as the single source of truth for the workspace side.
Is workspace-scoped slower?
Slightly, because each policy resolves membership. Keep it fast by resolving membership once in a stable security-definer function and indexing workspace_id. At normal scale the difference is negligible next to the flexibility you gain.