TL;DR: Row-level security (RLS) is a database feature that automatically filters which rows each user can see or modify, based on who they are: their identity, role, or group. Any app where different users share a database but shouldn’t see all the data needs RLS security.
Imagine your app’s database as a filing cabinet the whole office shares. Without any locks, anyone can read everything inside. You can lock an entire drawer, but then either everyone in the office can open it, or no one can. What do you do if people need to share a file drawer without seeing each other’s folders?
That’s the problem row-level security solves. It’s how sales reps see only their own deals and patients see only their own records, even though they all share the same table in a database.
This guide covers what row-level security is, how it compares to field-level and table-level controls, when to use it, common mistakes to avoid, and how Bubble handles it visually, with no SQL required.
What is row-level security?
Row-level security (RLS) is a database feature that automatically controls which rows a user can see or change, based on attributes like their user ID, role, or group.
Think of it as an invisible filter the database applies to every query. When a sales rep looks up the “Orders” table, the database limits results to rows where the owner matches their own user ID, so the rep gets back only their own orders, without ever knowing the filter exists.
Keeping that filter working is important. Broken access control (letting people see or change data they shouldn’t be able to) is the top security risk facing web applications. It usually happens when an app is responsible for enforcing a rule and simply forgets to, so it’s better to enforce the filter directly from the database.
Row-level security lives right inside the database. That means the same rule, called a predicate, applies no matter how someone connects to that database: through your app, a reporting tool, an API, or a direct query.
How does row-level security work?
Every time someone runs a query against an RLS database, the database checks the policy first. It looks at who’s asking (their user ID, role, or department) and filters the rows to match, before anything comes back. You never see rows you’re not allowed to, and you won’t get an error hinting they exist either.
Three things make this work:
- It’s automatic. The database applies the policy on every query, without anyone asking it to. A support agent looking up the “Tickets” table doesn’t have to filter to their own queue themselves. The database already limits results to tickets assigned to them.
- It knows who’s asking. The database reads session variables (a user ID, role, or department) the moment someone logs in. A support agent and their supervisor can run the exact same query against “Tickets” and see different results: The agent gets only their assigned tickets, while the supervisor sees the whole team’s queue.
- It works everywhere. The RLS policy applies no matter how someone connects to the database, not just through your app. That means a misconfigured frontend or a missed permission check in your code can’t accidentally expose rows it shouldn’t. The exception is privileged accounts, which can sometimes bypass policies depending on the system.
Take a multi-tenant SaaS app, where customers share the same “Projects” table. Each project has a tenant_id field marking which customer it belongs to. The RLS policy checks your tenant_id and only returns matching projects. Customer A never sees Customer B’s projects. That happens automatically, before any of your app’s own logic even runs.
Row-level security vs. field-level and table permissions
These three controls sound similar, but they’re not doing the same job. Row-level security decides which rows you get to see. Field-level security zooms in further, hiding specific fields within those rows. Table-level permissions lock down an entire table at once. Most apps end up using all three together.
Think back to the filing cabinet. Table permissions lock the whole drawer shut. Row-level security decides which folders you’re allowed to pull out, while field-level security blacks out specific lines once you’ve got the folder open.
| Control | What it restricts | Example |
|---|---|---|
| Table/object permissions | Access to an entire table or view | Block non-admin users from the "Payroll" table entirely |
| Row-level security | Which records within a table a user can see or modify | A sales rep sees only their own deals in the "Deals" table |
| Field-level security | Which fields within a row are visible | Hide the "Salary" field from all users except HR |
To set up these in order, start with table access first, then row policies to filter which records show up, and then field restrictions on anything sensitive.
Here’s what that looks like in a healthcare app: A clinician might get table-level permission to open a “Patients” table. But when she opens it, row-level security narrows what she sees down to just her own patients. Field-level security hides billing and insurance fields entirely, because she doesn’t need to see them.
When should you use row-level security?
If different users of your app need to see different slices of the same data, you probably need row-level security. The clearest sign is when you keep adding the same “only show this user’s own records” condition, over and over, in different parts of your app. That’s usually a signal the database should be enforcing that boundary for you, automatically.
Here are a few places RLS solves access problems:
- Multi-tenant SaaS apps: When multiple customers share the same database tables, row-level security keeps each tenant’s data invisible to everyone else. Say a project management tool stores Company A’s and Company B’s projects in the same “Projects” table. RLS guarantees Company A never sees Company B’s records, even if there’s a bug in the app that fails to filter correctly.
- Role-based employee access: Row-level security lets you scope data by department, seniority, or region, without duplicating the table. A regional sales manager sees only deals in their territory, while a national director sees deals across every region.
- Healthcare and regulated industries: In regulated spaces like apps that follow HIPAA, minimum-necessary access rules mean you’re expected to limit patient data access by role. Row-level security is a safer way to stay compliant than a manual check buried somewhere in your app’s code.
- Compliance and audit requirements: Centralizing your data-access policies also makes security audits easier. Your team has one place to check the rules instead of hunting through dozens of files, and you can point auditors to the database policies that govern every query.
What are the risks of row-level security?
Row-level security stops most accidental data exposure, but the stakes are still high enough that a bad setup matters. The global average cost of a data breach was $4.44 million in 2025, and it doesn’t take much of a misconfiguration to open a real gap. Here’s what to watch for:
- Admin and owner bypass: Some database engines let table owners and admins skip RLS policies entirely by default. Close that gap by turning on a “force” option in the engine, if it has one, or by writing your policies to explicitly cover admin accounts too. Test with both standard and privileged accounts, so you know the boundary holds.
- Missing write checks: Read and write access are often governed by separate policies. If you set up a read policy but skip the write policy, users might be blocked from viewing certain records while still able to edit them through a direct API call or a form. Set up both together, every time.
- Performance impact: The database checks a predicate on every single query, so a complex or unindexed policy condition can slow things down. If your policy filters on a tenant_id or owner_id column, index it to keep queries fast.
- Debugging opacity: When row-level security filters out a row, the user just sees nothing. There’s no error message and no hint that a hidden
Build for as long as you want on the Free plan. Only upgrade when you're ready to launch.
Join Bubble