Make your database refuse to leak one user’s data to another.
Row Level Security (RLS) is a PostgreSQL feature that decides, row by row, what each request is allowed to see or change - enforced inside the database, not just in your app code. Here’s what it is, why it matters, and how to turn it on safely.
What is Row Level Security, really?
Imagine one big spreadsheet shared by all your users. Normally, whoever opens it can read every row. RLS is a rule the database enforces so each person only ever sees the rows that belong to them - even if your app forgets to filter.
You write a policy once - for example, 'a row is visible only if its user_id matches the logged-in user.' PostgreSQL then applies it to every query automatically. No policy can be skipped by forgetting a WHERE clause.
Regular database permissions are all-or-nothing on a whole table. RLS is finer: everyone can use the same table, but each request only touches the rows it's allowed to.
You still check permissions in your app. RLS sits underneath as the last line of defense - so one missing check doesn't become a breach.
Most data leaks are a missing WHERE clause
The classic bug: an endpoint that trusts an id from the URL. Without RLS, one wrong line ships everyone's data. With RLS, the database quietly says no.
Without RLS
Your API runs SELECT * FROM invoices WHERE id = $1 using an id from the request. A user changes the id in the URL and reads someone else’s invoice. The database did exactly what it was told - the check that should have stopped it lived only in code that had a bug.
With RLS
The same query returns nothing, because a policy says a row is only visible when its account_id matches the current user. The attacker’s id doesn’t match, so the row simply isn’t there for them. The leak never happens - even though the app code was buggy.
Where RLS earns its keep
If any of these sound like your app, RLS is worth turning on.
Every customer's data lives in shared tables. RLS guarantees Acme Corp can never load Globex's rows, even if a query forgets the tenant filter.
Notes, files, orders, messages - anything that belongs to one person. A policy on user_id keeps each account's data private by default.
Read-only reporting where each viewer should only see their team's numbers. RLS scopes the same view to different audiences safely.
How missing RLS gets exploited
You don't need to be a hacker to trip these. They're the bugs that make the news.
Insecure Direct Object Reference: /invoices/1042 works, so the attacker tries /invoices/1043. Without RLS, the API happily returns a stranger's invoice. With RLS, it returns nothing.
A new query or a refactor drops the 'AND tenant_id = ...' condition. One deploy later, customers see each other's data. RLS makes that filter impossible to forget.
A leaked or too-broad token hits the database directly. If RLS is on and forced, the token still only reaches the rows its user is allowed to - the blast radius stays small.
Bulk endpoints (CSV export, admin search) are where filters get missed most. RLS scopes even those, so a reporting bug doesn't dump the whole table.
We detect it, recommend it, and guide you - we never force it
RLS is off by default on every table. Swyftstack's job is to make the right choice easy and the risky mistakes hard.
The Security tab in the database explorer lists every table and whether RLS is on, whether it's forced, and how many policies it has.
Tables that look like they hold per-user data (a user_id, tenant_id, or owner column) get a gentle 'recommended' nudge - never an automatic change.
Enable or disable RLS with one click, and start from clearly-labeled policy templates you edit and review before applying. PostgreSQL errors are shown in plain language.
Turn on RLS in three steps
You can do all of this from the Security tab of your database in the Swyftstack console - or run the SQL yourself. Here's what happens under the hood.
Enable and force RLS
Turn RLS on for the table, then force it so the policy also applies to the table owner (your app connects as the owner through its DATABASE_URL). In the console this is the “Enable RLS” button with “Force” checked.
Add at least one policy
A table with RLS on and no policies denies everything. Start from a template (owner-by-user_id, tenant isolation, or public-read/owner-write), replace the placeholder with your real column, review it, and apply.
Tell the database who the user is
At the start of each request, set the current user on the connection with set_config(’app.current_user_id’, ...). Your policy reads it back, and every query is scoped automatically from then on.
What the three steps look like
These are the exact statements. In the console, the first two are buttons and a reviewed template; the third lives in your app.
-- Enable RLS on the table ALTER TABLE notes ENABLE ROW LEVEL SECURITY; -- Force it so the policy also applies to the table owner -- (your app connects as the owner via DATABASE_URL) ALTER TABLE notes FORCE ROW LEVEL SECURITY;
The traps to avoid
Almost every RLS problem is one of these five. Knowing them up front saves an afternoon.
RLS on + zero policies = access denied for everyone but the owner. Always add a policy in the same change, and test before you rely on it.
The table owner bypasses non-forced RLS. Since your app connects as the owner, un-forced RLS does nothing for you. Force it, or use a separate non-owner role.
USING controls what rows are visible; WITH CHECK controls what rows can be written. Without WITH CHECK, a user could insert rows that belong to someone else. Set both.
Your policy filters on user_id or tenant_id on every query. If that column isn't indexed, large tables get slow. Add the index.
RLS is a safety net, not the whole trapeze. Keep your app-level authorization; let RLS catch what slips through.
If you test while connected as the owner without FORCE, everything 'works' because RLS is bypassed. Test as the role your app actually uses.
A short checklist
Do these and RLS becomes boring, in the best way.
Turn RLS on and force it in one change, so the owner role your app uses is actually covered.
Control reads and writes. A row a user can't see shouldn't be a row they can create for someone else.
Whatever your policy filters on should be indexed. It keeps scoped queries fast.
Use set_config at the start of each request so the connection carries the identity your policy needs.
Verify the policy from the role your app connects with, not as the owner - that's the only honest test.
Try it on a backup restore or a staging database, confirm nothing breaks, then apply to production.
RLS FAQ
Do I need Row Level Security?
If more than one user (or more than one customer) stores data in the same tables, RLS is worth serious consideration. It makes the database itself refuse to return one user's rows to another, so a single mistake in your app code can't turn into a data leak. If you're a solo app with a single trusted admin, you may not need it yet - which is exactly why Swyftstack leaves it off until you choose to turn it on.
Will turning on RLS break my app?
It can, if you enable it without a policy. A table with RLS on and no policies denies all access to non-owner roles. That's why Swyftstack shows you the status, lets you add a reviewed policy first, and explains that your app connects as the table owner. Enable RLS with FORCE and at least one policy, test in a branch or on a copy, then roll it out.
Why does Swyftstack not enable RLS automatically?
Because a correct policy depends on how YOUR app models users and tenants - there is no safe one-size-fits-all rule. Auto-enabling would either lock you out (RLS on, no policy) or give a false sense of safety (a generic policy that doesn't match your data). We detect it, recommend it where it looks useful, and guide you - but you stay in control.
My app connects as the database owner. Does RLS still apply?
Only if you also FORCE it. By default, the role that owns a table bypasses its own RLS policies, and Swyftstack databases are owned by the role in your DATABASE_URL. So enable RLS AND force it (one click in the Security tab), or connect your app with a separate non-owner role. Swyftstack's UI flags this for you.
Is RLS a replacement for checking permissions in my app?
No - it's a safety net underneath them. Keep your application checks; RLS is the last line of defense for when an app check is missing or wrong. Defense in depth: the app decides intent, the database enforces the boundary.
Does RLS slow down my queries?
A policy adds a condition to every query on the table, so it has a cost - usually small if the columns your policy filters on (like user_id or tenant_id) are indexed. Index those columns, keep policy expressions simple, and the overhead is typically negligible for normal workloads.
Give your data a floor it can't fall through.
Detect where RLS helps, turn it on with guidance, and keep control the whole way. It's built into every Swyftstack database.