The most common multi-tenant data isolation pattern in SaaS is application-layer enforcement: every query includes a WHERE org_id = ? clause, and the application code is responsible for ensuring the right org_id is always passed. This works until it does not.
Application code has bugs. New engineers forget the WHERE clause. A refactor introduces a query path that bypasses the check. An ORM generates a query without the expected filter. These are not hypothetical scenarios — they are the root cause of most multi-tenant data leaks.
What RLS Actually Does
Row-Level Security is a PostgreSQL feature that attaches access policies to tables. When enabled, every query against the table — regardless of where it comes from — is filtered by the policy. The database engine enforces the policy before returning results. The exception is the role: a superuser or a role with BYPASSRLS skips every policy, and the table owner does too unless the table has FORCE ROW LEVEL SECURITY. Which role the application connects as decides whether RLS protects anything.
-- Enable RLS on the table
ALTER TABLE api_requests ENABLE ROW LEVEL SECURITY;
-- Create policy: users can only see their own org's rows
CREATE POLICY org_isolation ON api_requests
USING (org_id = current_setting('app.current_org_id')::uuid);
-- Set the org context at the start of each request
SET LOCAL app.current_org_id = '550e8400-e29b-41d4-a716-446655440000';
-- Now this query automatically filters to the current org
SELECT * FROM api_requests WHERE created_at > NOW() - INTERVAL '1 day';G8KEPR's Implementation
G8KEPR enforces tenant isolation in the application layer: tenant-scoped queries carry a WHERE organization_id filter for the caller's organization. PostgreSQL RLS policies on the tenant tables are defence-in-depth. They are enforced when the application connects as the NOBYPASSRLS g8kepr_service role that the self-hosted Helm chart creates; on the hosted instance the application role has BYPASSRLS, so there the application-layer filter is the enforcement.
Trade-offs
RLS adds a policy predicate to every query on a protected table. Its cost depends on the policy expression and on whether the tenant column is indexed, so measure it on your own workload. Queries against non-RLS tables (e.g., lookup tables, public data) are unaffected.
If you are designing a multi-tenant SaaS database schema today, enable RLS from the start. Retrofitting it onto a schema where application code already assumes no RLS is painful. Getting it right at schema creation is a one-time investment that pays for itself the first time an application bug would have been a data breach.
