Neon’s Row-Level Security (RLS) allows you to enforce data isolation between tenants at the database level, not just at the application level.
Let’s see it in action. Imagine a products table shared across multiple tenants in your Neon database.
CREATE TABLE products (
id serial primary key,
name text not null,
price numeric not null,
tenant_id uuid not null
);
-- Populate with some data for two tenants
INSERT INTO products (name, price, tenant_id) VALUES
('Laptop', 1200.00, 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'),
('Keyboard', 75.00, 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'),
('Monitor', 300.00, 'f47ac10b-58cc-4372-a567-0e02b2c3d479'),
('Webcam', 50.00, 'f47ac10b-58cc-4372-a567-0e02b2c3d479');
Without RLS, any user connected to this database could see all products. With RLS, we can restrict this. The core idea is to associate a tenant_id with the current database session and then filter all queries based on it.
First, we need to enable RLS on the products table:
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
Now, we create a policy that dictates who can see what. This policy will look at the tenant_id column in the products table and compare it to a value that we’ll set for the current session.
CREATE POLICY tenant_isolation_policy ON products
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
The USING clause is the crucial part. It specifies the condition that must be true for a row to be visible. current_setting('app.current_tenant_id') is a PostgreSQL function that retrieves a configuration parameter set for the current session. We cast it to uuid to match the tenant_id column type.
The missing piece is how to set that app.current_tenant_id for each user or request. This is typically done in your application’s connection logic. When a user logs in or a request comes in for a specific tenant, your application code would execute a command to set this session-level variable before running any queries against the products table.
For example, if your application code is handling a request for tenant a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11, it would run:
SET app.current_tenant_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';
After this SET command, any subsequent SELECT, UPDATE, or DELETE on products would automatically be filtered by the tenant_isolation_policy.
-- If app.current_tenant_id is set to 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
SELECT * FROM products;
This would return:
id | name | price | tenant_id
----+----------+---------+--------------------------------------
1 | Laptop | 1200.00 | a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
2 | Keyboard | 75.00 | a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
(2 rows)
If the SET command was for f47ac10b-58cc-4372-a567-0e02b2c3d479:
SET app.current_tenant_id = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
SELECT * FROM products;
You’d see:
id | name | price | tenant_id
----+--------+-------+--------------------------------------
3 | Monitor| 300.00| f47ac10b-58cc-4372-a567-0e02b2c3d479
4 | Webcam | 50.00| f47ac10b-58cc-4372-a567-0e02b2c3d479
(2 rows)
The beauty of this approach is that the RLS policy is enforced by the database itself. Even if a developer makes a mistake in their application code and forgets to add a WHERE tenant_id = ... clause, the RLS policy will still protect the data. This significantly reduces the attack surface for data leakage between tenants.
The app.current_tenant_id configuration parameter isn’t a built-in PostgreSQL setting; you need to define it. This is done by adding it to your postgresql.conf file (or a file included by it, like neon.conf in Neon’s managed environment) with a SET command or via a ALTER SYSTEM SET command, and then restarting the database. However, for session-specific values like tenant IDs, it’s often cleaner to manage it directly within your application’s connection pool setup. Many connection pool libraries allow you to execute a specific SQL command upon acquiring a connection from the pool, which is the ideal place to SET app.current_tenant_id.
A common pitfall is forgetting to unset or reset the app.current_tenant_id when a connection is returned to the pool. If you don’t, the next tenant using that same connection might accidentally see data from the previous tenant. This is why careful connection management and ensuring the SET command is executed for every connection used for tenant-specific operations is critical.
Once RLS is enabled and policies are in place, you might encounter errors if your application attempts to INSERT or UPDATE rows without specifying a tenant_id that matches the current session’s app.current_tenant_id.