TLDR: One missing RLS policy silently exposes every tenant's data to every other tenant — and your API returns 200 OK the whole time. Vibe44 ships 32 Supabase tables with RLS enabled and tenant-isolation policies in place by default. That coverage, backed by 900 Vitest unit tests and 9 Playwright E2E suites, closes the gap between "auth works" and "data is isolated" in a 2026 landscape where data breaches are ruthlessly automated.
One missing RLS policy and every tenant can read every other tenant's data. The scariest bugs are the ones that quietly return 200 OK. No stack trace. No 403. Just clean JSON that happens to contain someone else's records. That is the failure mode app-layer auth cannot catch by itself — and it is exactly what database-layer Row-Level Security exists to prevent.
Why isn't app-layer auth enough?
Your middleware checks a JWT. Your API route validates the session. Your service function filters by the current user. That chain works — until it doesn't.
A new developer adds a reporting endpoint and forgets the WHERE tenant_id = $1 clause. A caching layer returns a shared query result. A background job runs a broad query under a service account. Any one of these breaks tenant isolation without touching your auth logic. Your auth layer keeps saying "yes, this request is authenticated." The question it cannot answer is whether the authenticated user is reading only their own rows.
Defense-in-depth means each layer enforces the constraint independently. The application layer enforces it in code. The database layer enforces it in policy. Neither trusts the other to do the job. If the application layer is compromised or simply wrong, the database falls back to its own rules and rejects the query. That is the guarantee Row-Level Security gives you.
RLS policies live inside PostgreSQL itself. They execute on every SELECT, INSERT, UPDATE, and DELETE before a row reaches the caller. The database evaluates them regardless of which application, service, or direct connection issued the query. You cannot accidentally skip them by restructuring your API. They are not middleware you can forget to mount.
For a multi-tenant SaaS this is not optional hardening. It is the boundary that makes tenant isolation provable rather than assumed.
What do RLS patterns for multi-tenant SaaS actually look like?
The foundation is a tenant_id column on every table that holds tenant-scoped data. Every policy references it. A SELECT policy permits a row only when the row's tenant_id matches a value the database can verify from the current session context — typically a claim extracted from the JWT via a helper function like auth.jwt() ->> 'tenant_id' or a custom current_tenant_id() function you define and set during connection initialization.
Helper functions matter because repeating the same JWT-extraction expression in every policy is fragile. One function, called by all policies, means you fix JWT claim structure in one place. You can also wrap the function in a SECURITY DEFINER block so it runs with fixed permissions, though that choice carries its own tradeoffs (more on that below).
Write policies (INSERT, UPDATE, DELETE) need the same discipline as read policies. A tenant that can only SELECT their own rows but can INSERT rows with an arbitrary tenant_id can quietly poison another tenant's dataset. Every operation needs a corresponding policy.
The service-role hazard is the most common blind spot. Supabase's service-role key bypasses RLS entirely — that is its purpose. Any server-side code that initializes the Supabase client with the service-role key operates outside all your carefully written policies. This is necessary for admin operations and migrations. It is a data-leak waiting to happen if you reach for the service-role client in a user-facing request handler. The anon key and the authenticated user's JWT-scoped client are the right tools for user-facing queries. Reserve service-role for deliberate elevated operations, keep that boundary explicit, and audit it during code review.
How does Vibe44 ship RLS across all 32 tables by default?
Vibe44's data layer spans 32 tables covering users, teams, subscriptions, billing, AI usage, audit logs, and feature flags. Every one of them ships with RLS enabled and tenant-scoping policies already written. You do not opt in to isolation — you opt out if you deliberately need to, and that decision is visible in the codebase.
The schema definitions live in lib/db/schema/. Each file defines the table structure, the tenant_id column where applicable, and the foreign-key relationships. The migration files in supabase/migrations/ contain the corresponding ALTER TABLE ... ENABLE ROW LEVEL SECURITY statements and the CREATE POLICY blocks. The migration history is the audit trail. You can read exactly which policies exist, when they were added, and what they permit.
Auth context flows through lib/auth/. Session tokens carry tenant claims that get passed to the Supabase client on authenticated requests. The database extracts those claims in policy checks using a consistent helper function defined once and referenced everywhere. No per-policy JWT parsing, no per-route filter to forget.
The approach on the Vibe44 auth architecture is covered in depth in Auth That Doesn't Leak: Sessions, MFA & PKCE on Supabase + Next.js — worth reading if you want to understand how the JWT-to-policy chain is constructed from the auth side.
Coverage across all 32 tables means the isolation guarantee does not have gaps. A partial RLS implementation — 28 tables covered, 4 missed — gives you false confidence. An attacker or a bug does not need many tables. They need one. Vibe44's starting point is complete coverage so you are hardening from a secure baseline, not patching holes.
This is also why the Ship a Multi-Tenant SaaS in a Weekend: Architecture That Doesn't Fall Over post treats RLS as structural, not optional — the multi-tenant architecture only holds if the isolation layer is comprehensive.
How do you test RLS so it can't silently break?
RLS policies are database objects. Like any other code, they can be wrong when written and can regress when changed. Testing them is not optional if you want the guarantee to hold.
The test approach that works: create two tenants in a test database, insert rows belonging to each, then execute queries as each tenant's authenticated context and assert that only the correct rows are returned. You are testing the database's enforcement of your policy, not your application code's filtering logic. These are different things.
Vibe44 ships 900 unit tests (Vitest) and 9 end-to-end suites (Playwright). A portion of those tests exercise the RLS layer directly — asserting that cross-tenant queries return empty result sets, that inserts with a mismatched tenant_id are rejected, and that service-role operations succeed where the anon client would be blocked. The test suite makes policy regressions visible. If a migration accidentally drops or alters a policy, the tests fail before the change reaches production.
The practical discipline: every new table added to lib/db/schema/ needs a corresponding migration in supabase/migrations/ that enables RLS and defines policies, and those policies need test coverage before the PR merges. That is the checklist that keeps 32 tables at 32.
What are the common RLS pitfalls that burn teams?
Performance on large tables. RLS policies execute on every query. A poorly written policy that calls an expensive function — especially one that hits another table — can turn a fast SELECT into a full scan. The fix is to keep policy expressions cheap: reference indexed columns, avoid subqueries in policies where a helper function returning a session variable will do, and use EXPLAIN ANALYZE to catch policy-driven performance regressions before they hit production.
Recursive policies. A policy on Table A that queries Table B, which has its own policy that queries Table A, causes infinite recursion. PostgreSQL will error, not silently loop, but debugging the error message requires understanding your policy graph. Keep policy helper functions simple and avoid cross-table references inside policy expressions where possible. If you need cross-table logic, push it into application queries with explicit JOIN conditions rather than hiding it in policy rules.
SECURITY DEFINER bypass leaks. A function marked SECURITY DEFINER runs with the privileges of the function's owner, not the caller. If you use SECURITY DEFINER for your current_tenant_id() helper and that function has a bug — returns a null, or can be manipulated by function arguments — the policy it powers is compromised. Audit every SECURITY DEFINER function. They are elevated-privilege code.
Forgetting to set the search path. A SECURITY DEFINER function with a mutable search_path can be hijacked by schema injection. Always set search_path = '' and use fully qualified names (public.your_table) inside SECURITY DEFINER functions. PostgreSQL documentation calls this out explicitly. Most teams learn it the hard way.
Service-role sprawl. As a codebase grows, the service-role client tends to spread. Someone pastes an initialization snippet from a working admin route into a user-facing handler. The application keeps working. RLS is silently bypassed. Audit every Supabase client initialization in your codebase and confirm which key it uses. The anon/user key is the default; service-role is the deliberate exception.
A tenant data leak is not a performance bug you fix in the next sprint. It is a trust event. Depending on your jurisdiction and the data involved, it may be a breach disclosure obligation. The cost of investigating, notifying, and repairing trust dwarfs the cost of getting the policy layer right before launch.
Vibe44 ships that layer already done — 32 tables, RLS on by default, policies in version-controlled migrations, and a test suite that catches regressions. The $197 one-time license covers the security foundation that top founders are already building on.
Clone the repo, open supabase/migrations/, and read the policies before you write your first one from scratch.
Frequently asked questions
1. Does enabling RLS on every table slow down my Supabase database?
RLS adds a policy evaluation step to every query. For simple policies referencing indexed columns — like tenant_id = current_tenant_id() where tenant_id is indexed — the overhead is negligible. The risk is complex policies with subqueries or unindexed lookups. Index your tenant_id columns, keep policy expressions simple, and run EXPLAIN ANALYZE on queries touching large tables. Vibe44's policies are written with this constraint in mind.
2. Can I use Supabase's service-role key in Next.js API routes?
You can, but you should not as a default. The service-role key bypasses RLS entirely. Use it only for explicitly elevated operations — seed scripts, admin jobs, migration runners — and initialize it only in the contexts that need it. User-facing API routes should use the authenticated Supabase client initialized with the user's JWT, which keeps RLS in force.
3. How many RLS policies does Vibe44 ship?
Vibe44 covers 32 tables with RLS enabled by default. The exact policy count is higher than 32 because each table has separate policies for SELECT, INSERT, UPDATE, and DELETE operations. You can count them by reviewing the CREATE POLICY statements in supabase/migrations/.
4. What happens if a developer adds a new table without enabling RLS?
By default in Supabase, a new table has RLS disabled — meaning all rows are accessible to any authenticated user. Vibe44's migration conventions and test suite are the guardrails: new tables are expected to follow the pattern, and tests that exercise cross-tenant isolation will expose gaps. Code review with explicit RLS checklist items is the human layer on top.
5. Does Vibe44's RLS setup work with PostgreSQL row-level security outside Supabase?
The policies are standard PostgreSQL 15 RLS syntax and will run on any PostgreSQL 15+ instance. Supabase-specific helpers like auth.jwt() would need to be replaced with equivalent session-variable mechanisms if you move off Supabase. The migration files in supabase/migrations/ document exactly what those helpers are, so adapting them is a known, bounded task.






