Back to Blog
Engineering

Building a Multi-Tenant Platform with Supabase RLS

Hugo·Mar 5, 2026·6 min read

When we set out to build Hugo, one of the earliest architectural decisions was how to handle multi-tenancy. We needed to support thousands of organizations on a single platform, each with complete data isolation, without the operational overhead of per-tenant databases.

We chose Supabase with PostgreSQL Row Level Security (RLS) — and after a year in production, we're confident it was the right call. Here's how it works and what we've learned.

Every table in Hugo's database includes an `org_id` column that identifies which organization owns each row. RLS policies ensure that any query — whether it comes from our API, a direct database connection, or a serverless function — can only access rows belonging to the authenticated user's organization.

The RLS policy is elegantly simple: `CREATE POLICY org_isolation ON events FOR ALL USING (org_id = auth.jwt() ->> 'org_id')`. This single line ensures that no matter how complex the query, no data leaks between organizations.

Performance was our biggest concern. RLS adds overhead to every query because PostgreSQL must evaluate the policy for each row. We mitigated this with composite indexes that include `org_id` as the leading column: `CREATE INDEX idx_events_org_date ON events (org_id, start_at)`. This lets the query planner use the index to satisfy both the RLS filter and the application filter in a single scan.

We also learned some hard lessons. Aggregate queries that span the entire table (like `COUNT(*)`) can be slow because RLS prevents the planner from using certain optimizations. Our solution: materialized views that pre-aggregate per-organization statistics, refreshed on a schedule.

For migrations, we use a strict protocol: every new table gets RLS enabled before any data is inserted, and every migration is tested with multiple organization contexts to verify isolation. We've built CI checks that fail the build if a table is created without an RLS policy.

The result is a system where data isolation is guaranteed at the database level, not the application level. Even if a bug in our API layer constructs a bad query, RLS prevents cross-tenant data exposure. It's defense in depth, and it lets our engineers move fast without worrying about security regressions.

All postsTry Hugo free