Skip to main content
ClaudeChatGPT

RLS Examples

Use this page to review practical Row-Level Security examples before you define your own policies.

Plan support

Data Security is available in Enterprise Cloud and Enterprise Plus plans. Existing Business plan customers keep access.

This page provides six diverse Row-Level Security examples. Each example includes:

  • A description of the use case
  • The session properties it requires
  • The policy condition
  • A rough table schema
  • An example query before RLS
  • The compiled query after applying session properties

1. Organization isolation (multi-tenant)

Ensures that each customer (organization) can only view their own data. This is the most common multi-tenant isolation scenario.

Session properties

  • @user_org_id (string)

Condition

-- org_id is the column name in the table
-- @user_org_id is the session property name
org_id = @user_org_id

Table schema

orders(
id INT,
org_id STRING,
amount NUMBER
)

Example query

SELECT * FROM orders;

Compiled query (with @user_org_id = 'org_123')

SELECT * FROM orders WHERE org_id = 'org_123';

2. Owner access

Restricts rows to those owned by the user. For example, sales reps only see deals they personally own.

Session properties

  • @user_id (string)

Condition

-- owner_user_id is the column name in the table
-- @user_id is the session property name
owner_user_id = @user_id

Table schema

deals(
id INT,
owner_user_id STRING,
value NUMBER
)

Example query

SELECT * FROM deals;

Compiled query (with @user_id = 'u_42')

SELECT * FROM deals WHERE owner_user_id = 'u_42';

3. Team membership access

Allows access if the user owns the row or belongs to a team that has access. Useful when teams share responsibility for certain data (e.g., a support ticket queue).

Session properties

  • @team_ids (string) — comma-separated list of team IDs
  • @user_id (string)

Condition

-- owner_user_id and team_id are the column names in the table
-- @user_id is the session property name
owner_user_id = @user_id
OR team_id IN (SELECT value FROM SPLIT(@team_ids, ','))

Table schema

tickets(
id INT,
owner_user_id STRING,
team_id STRING,
subject STRING
)

Example query

SELECT * FROM tickets;

Compiled query (with @user_id = 'u_42', @team_ids = 't1,t2')

SELECT * FROM tickets
WHERE owner_user_id = 'u_42'
OR team_id IN (SELECT value FROM SPLIT('t1,t2', ','));

4. Region allow-list

Restricts access to rows belonging to specific regions. Common in global organizations where regional managers only see data for their assigned territories.

Session properties

  • @region_ids (string) — comma-separated list of regions

Condition

-- region_id is the column name in the table
-- @region_ids is the session property name
region_id IN (SELECT value FROM SPLIT(@region_ids, ','))

Table schema

customers(
id INT,
name STRING,
region_id STRING
)

Example query

SELECT * FROM customers;

Compiled query (with @region_ids = 'US,CA')

SELECT * FROM customers
WHERE region_id IN (SELECT value FROM SPLIT('US,CA', ','));

5. RBAC-driven access

Grants access based on assigned roles. Rows tagged with required_role are only visible if the user holds that role (e.g., finance data visible only to FINANCE_ANALYST).

Session properties

  • @role_ids (string) — comma-separated list of role names

Condition

-- required_role is the column name in the table
-- @role_ids is the session property name
required_role IN (SELECT value FROM SPLIT(@role_ids, ','))

Table schema

reports(
id INT,
report_name STRING,
required_role STRING
)

Example query

SELECT * FROM reports;

Compiled query (with @role_ids = 'FINANCE_ANALYST,HR_ADMIN')

SELECT * FROM reports
WHERE required_role IN (SELECT value FROM SPLIT('FINANCE_ANALYST,HR_ADMIN', ','));

6. Entitlement mapping table

Use this pattern when a user is entitled to a large set of IDs (hundreds or thousands) and enumerating them in the session would make the request header too large.

A common way to scope multi-account access is to pass every entitled ID as a session property, then filter with an IN (...) list, as in example 4. This works for small sets, but the session properties travel on the request (for API calls, in the X-Wren-Session-Properties header). When a user is entitled to 10,000+ accounts, the enumerated list makes the header very large, which is fragile and can hit request-size limits.

Instead, keep the session property to a single key — the customer/tenant ID — and move the account set into an entitlement mapping table modeled in the same data source. RLS resolves the account set with a subquery at query time, so the header stays tiny regardless of how many accounts the customer is entitled to.

Session properties

  • @customer_id (string) — one value per user; header size is constant

Entitlement model

Add the mapping table as a model in the same data source as the protected model, so the compiled SQL can reference it in a subquery.

account_entitlements(
customer_id STRING,
account_id STRING
)

Condition

-- account_id is the column name in the protected table
-- account_entitlements is the mapping model in the same data source
-- @customer_id is the session property name
-- Qualify columns in the subquery: an unqualified name missing from account_entitlements
-- would silently resolve as a correlated reference to the outer row instead of erroring.
account_id IN (
SELECT account_entitlements.account_id
FROM account_entitlements
WHERE account_entitlements.customer_id = @customer_id
)

Table schema (protected model)

transactions(
id INT,
account_id STRING,
amount NUMBER
)

Example query

SELECT * FROM transactions;

Compiled query (with @customer_id = 'cust_123')

SELECT * FROM transactions
WHERE account_id IN (
SELECT account_entitlements.account_id
FROM account_entitlements
WHERE account_entitlements.customer_id = 'cust_123'
);

Why this helps

  • Constant header size. The request carries one value (@customer_id) instead of the full account list, so header size no longer scales with the number of entitled accounts.
  • Single source of truth. Entitlements live in a table you already govern and can update independently, rather than being recomputed and injected on every request.
  • Same enforcement path. The condition is applied by Wren Engine before the query reaches your warehouse, exactly like the other examples on this page — moving the account set into a table changes where the list lives, not where access is enforced.

Considerations

  • Protect the mapping model itself. account_entitlements is a regular model, so without its own policy any user can query it directly and read every customer's entitlements. Give it an RLS rule of its own (customer_id = @customer_id) or restrict it so it isn't queryable.
  • Keep entitlements current. Access is only as correct as the mapping table. Treat writes to account_entitlements as a governed process (revocations take effect on the next query).
  • Performance. Ensure account_entitlements is indexed/clustered on customer_id (and account_id) in your warehouse so the subquery stays cheap on large tables.
  • Composite scoping. If a customer's entitlements depend on more than one dimension, extend the mapping table with additional columns and add matching predicates to the subquery WHERE clause.

When to use the inline list instead

The SPLIT(@account_ids, ',') approach is simpler and avoids modeling an extra table. Prefer it when the entitled set per user is small and stable. Switch to the entitlement mapping table when the set is large, changes often, or is already maintained elsewhere in the same data source.