Skip to content

Tenant Row-Level Security (RLS) Policy API Documentation

Tenant Row-Level Security (RLS) Policy API Documentation

Version: v1 Base Path: /api/v1/tenants/{id}/rls/policies Authentication: Required Rate Limit: 50 requests/minute per tenant

Overview

The Tenant RLS Policy API provides Row-Level Security policy management for fine-grained data access control. RLS policies automatically filter query results based on user context, ensuring complete tenant isolation and preventing cross-tenant data leakage.

Table of Contents


Endpoints

POST /tenants/{id}/rls/policies

Creates a new Row-Level Security policy for a tenant.

Request

POST /api/v1/tenants/{id}/rls/policies HTTP/1.1
Host: api.heliosdb.com
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "tenant_isolation_users",
"table": "users",
"expression": "tenant_id = {tenant_id}",
"operations": ["SELECT", "UPDATE", "DELETE"],
"allow_superuser_bypass": true,
"description": "Isolate users by tenant_id column"
}

Path Parameters:

  • id (UUID, required): Tenant identifier

Request Body:

FieldTypeRequiredDescription
namestringYesPolicy name (3-128 chars, alphanumeric, underscores, hyphens)
tablestringYesTarget table name (1-255 chars, alphanumeric, underscores)
expressionstringYesSQL WHERE clause condition (1-2048 chars)
operationsarray[string]YesOperations: SELECT, INSERT, UPDATE, DELETE
allow_superuser_bypassbooleanNoAllow superuser bypass (default: true)
descriptionstringNoPolicy description (max 512 chars)

Response

Success (201 Created):

{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policy_id": "users_tenant_isolation_users",
"name": "tenant_isolation_users",
"table": "users",
"enabled": true,
"created_at": "2025-12-09T10:30:00Z",
"message": "RLS policy created successfully"
}

Error Responses:

  • 400 Bad Request: Invalid policy or validation error
  • 404 Not Found: Tenant does not exist
  • 409 Conflict: Policy already exists
  • 500 Internal Server Error: Server-side error

GET /tenants/{id}/rls/policies

Lists all RLS policies for a tenant with optional filtering.

Request

GET /api/v1/tenants/{id}/rls/policies?table=users&enabled=true HTTP/1.1
Host: api.heliosdb.com
Authorization: Bearer {token}

Path Parameters:

  • id (UUID, required): Tenant identifier

Query Parameters:

ParameterTypeDescription
tablestringFilter by table name
enabledbooleanFilter by enabled status
operationstringFilter by operation (SELECT, INSERT, UPDATE, DELETE)

Response

Success (200 OK):

{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policies": [
{
"policy_id": "users_tenant_isolation",
"name": "tenant_isolation",
"table": "users",
"expression": "tenant_id = {tenant_id}",
"operations": ["SELECT", "UPDATE", "DELETE"],
"allow_superuser_bypass": true,
"enabled": true,
"description": "Tenant isolation for users table"
},
{
"policy_id": "orders_customer_access",
"name": "customer_access",
"table": "orders",
"expression": "(tenant_id = {tenant_id} AND status = 'active') OR owner_id = {user_id}",
"operations": ["SELECT"],
"allow_superuser_bypass": false,
"enabled": true,
"description": "Customer access control for orders"
}
],
"total_count": 2
}

Error Responses:

  • 404 Not Found: Tenant does not exist
  • 500 Internal Server Error: Server-side error

DELETE /tenants/{id}/rls/policies/{policy_id}

Deletes an existing RLS policy.

Request

DELETE /api/v1/tenants/{id}/rls/policies/users_tenant_isolation HTTP/1.1
Host: api.heliosdb.com
Authorization: Bearer {token}

Path Parameters:

  • id (UUID, required): Tenant identifier
  • policy_id (string, required): Policy ID in format “{table}_{policy_name}“

Response

Success (200 OK):

{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policy_id": "users_tenant_isolation",
"policy_name": "tenant_isolation",
"table": "users",
"message": "RLS policy 'users_tenant_isolation' deleted successfully",
"deleted_at": "2025-12-09T10:35:00Z"
}

Error Responses:

  • 404 Not Found: Tenant or policy does not exist
  • 500 Internal Server Error: Server-side error

Data Models

RlsPolicy

FieldTypeDescriptionConstraints
policy_idstringUnique policy identifierFormat: “{table}_{policy_name}“
namestringPolicy name3-128 chars, alphanumeric, _, -
tablestringTarget table1-255 chars, alphanumeric, _
expressionstringSQL WHERE condition1-2048 chars, valid SQL
operationsarray[RlsOperation]Applicable operationsSELECT, INSERT, UPDATE, DELETE
allow_superuser_bypassbooleanSuperuser bypass flagtrue/false
enabledbooleanPolicy enabled statustrue/false
descriptionstringPolicy descriptionmax 512 chars

RlsOperation

Enum of operations that can have RLS policies:

  • SELECT: Applied to SELECT queries
  • INSERT: Applied to INSERT statements
  • UPDATE: Applied to UPDATE statements
  • DELETE: Applied to DELETE statements

RLS Expressions

Expression Syntax

RLS expressions are SQL WHERE clause conditions that are automatically added to queries.

Supported Placeholders:

  • {tenant_id}: Current tenant’s UUID
  • {user_id}: Current user’s identifier
  • {role}: Current user’s role
  • {timestamp}: Current timestamp

Expression Examples

1. Basic Tenant Isolation

tenant_id = {tenant_id}

Description: Filters all rows to current tenant only.

Applied Query:

-- Original
SELECT * FROM users WHERE status = 'active';
-- Rewritten
SELECT * FROM users WHERE (tenant_id = '550e8400-e29b-41d4-a716-446655440000') AND status = 'active';

2. User and Tenant Ownership

tenant_id = {tenant_id} AND (owner_id = {user_id} OR public = true)

Description: Allow access to tenant’s public data or user’s owned data.

3. Department-Based Access

tenant_id = {tenant_id} AND department_id IN (
SELECT id FROM departments WHERE tenant_id = {tenant_id}
)

Description: Restrict access based on department membership.

4. Time-Based Access

tenant_id = {tenant_id} AND valid_from <= {timestamp} AND valid_until >= {timestamp}

Description: Filter data based on temporal validity.

5. Hierarchical Access

tenant_id = {tenant_id} AND (
created_by = {user_id} OR
manager_id = {user_id} OR
{role} = 'admin'
)

Description: Allow access to own records, subordinate records, or all records for admins.

Expression Validation

The API validates expressions for:

  1. SQL Injection Prevention: Blocks dangerous keywords (DROP, TRUNCATE, etc.)
  2. Syntax Checking: Ensures valid SQL WHERE clause syntax
  3. Placeholder Presence: Warns if no tenant_id/user_id placeholders found
  4. Length Limits: Maximum 2048 characters
  5. Empty/Whitespace: Rejects empty or whitespace-only expressions

Query Rewriting

How It Works

When a query is executed with RLS policies enabled:

  1. Policy Lookup: Identify applicable policies for the query’s tables
  2. Context Injection: Replace placeholders with actual values
  3. Expression Combination: Combine policy expressions with AND
  4. Query Rewrite: Inject combined expression into WHERE clause
  5. Execution: Execute rewritten query

Rewriting Examples

Example 1: Simple SELECT

Original Query:

SELECT * FROM users WHERE email LIKE '%@example.com';

RLS Policy:

{
"table": "users",
"expression": "tenant_id = {tenant_id}",
"operations": ["SELECT"]
}

Rewritten Query:

SELECT * FROM users
WHERE (tenant_id = '550e8400-e29b-41d4-a716-446655440000')
AND email LIKE '%@example.com';

Example 2: JOIN Query

Original Query:

SELECT u.*, o.order_id
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed';

RLS Policies:

  • Users table: tenant_id = {tenant_id}
  • Orders table: tenant_id = {tenant_id}

Rewritten Query:

SELECT u.*, o.order_id
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE (u.tenant_id = '550e8400-e29b-41d4-a716-446655440000')
AND (o.tenant_id = '550e8400-e29b-41d4-a716-446655440000')
AND o.status = 'completed';

Example 3: UPDATE Statement

Original Query:

UPDATE users SET last_login = NOW() WHERE id = 123;

RLS Policy:

{
"table": "users",
"expression": "tenant_id = {tenant_id}",
"operations": ["UPDATE"]
}

Rewritten Query:

UPDATE users
SET last_login = NOW()
WHERE (tenant_id = '550e8400-e29b-41d4-a716-446655440000')
AND id = 123;

Examples

Example 1: Create Basic Tenant Isolation Policy

Terminal window
curl -X POST https://api.heliosdb.com/api/v1/tenants/550e8400-e29b-41d4-a716-446655440000/rls/policies \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "tenant_isolation",
"table": "products",
"expression": "tenant_id = {tenant_id}",
"operations": ["SELECT", "UPDATE", "DELETE"],
"allow_superuser_bypass": true,
"description": "Basic tenant isolation for products table"
}'

Response:

{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policy_id": "products_tenant_isolation",
"name": "tenant_isolation",
"table": "products",
"enabled": true,
"created_at": "2025-12-09T10:30:00Z",
"message": "RLS policy created successfully"
}

Example 2: Create Complex Access Control Policy

Terminal window
curl -X POST https://api.heliosdb.com/api/v1/tenants/550e8400-e29b-41d4-a716-446655440000/rls/policies \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "document_access",
"table": "documents",
"expression": "(tenant_id = {tenant_id} AND status = '\''active'\'') OR owner_id = {user_id}",
"operations": ["SELECT", "UPDATE"],
"allow_superuser_bypass": false,
"description": "Access control for active documents or owned documents"
}'

Example 3: List Policies for a Table

Terminal window
curl -X GET "https://api.heliosdb.com/api/v1/tenants/550e8400-e29b-41d4-a716-446655440000/rls/policies?table=users" \
-H "Authorization: Bearer ${TOKEN}"

Response:

{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policies": [
{
"policy_id": "users_tenant_isolation",
"name": "tenant_isolation",
"table": "users",
"expression": "tenant_id = {tenant_id}",
"operations": ["SELECT", "UPDATE", "DELETE"],
"allow_superuser_bypass": true,
"enabled": true,
"description": "Tenant isolation for users table"
}
],
"total_count": 1
}

Example 4: Delete an RLS Policy

Terminal window
curl -X DELETE https://api.heliosdb.com/api/v1/tenants/550e8400-e29b-41d4-a716-446655440000/rls/policies/products_tenant_isolation \
-H "Authorization: Bearer ${TOKEN}"

Response:

{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policy_id": "products_tenant_isolation",
"policy_name": "tenant_isolation",
"table": "products",
"message": "RLS policy 'products_tenant_isolation' deleted successfully",
"deleted_at": "2025-12-09T10:35:00Z"
}

Best Practices

1. Policy Design

DO:

  • ✅ Create one policy per table for tenant isolation
  • ✅ Use descriptive policy names
  • ✅ Include {tenant_id} placeholder in all multi-tenant policies
  • ✅ Test policies in staging before production
  • ✅ Document complex expressions

DON’T:

  • ❌ Create overly complex expressions that hurt performance
  • ❌ Use subqueries unless necessary
  • ❌ Duplicate policies across tables (use table inheritance)
  • ❌ Hard-code values in expressions

2. Security

DO:

  • ✅ Enable RLS on all tables containing tenant data
  • ✅ Set allow_superuser_bypass = false for sensitive data
  • ✅ Validate policy expressions before creation
  • ✅ Regularly audit active policies
  • ✅ Use parameterized placeholders ({tenant_id}, {user_id})

DON’T:

  • ❌ Rely solely on application-layer filtering
  • ❌ Disable RLS without thorough review
  • ❌ Allow user input in policy expressions
  • ❌ Create policies that can be circumvented

3. Performance

DO:

  • ✅ Add indexes on columns used in RLS expressions
  • ✅ Keep expressions simple and efficient
  • ✅ Monitor query performance after adding policies
  • ✅ Use covering indexes for common access patterns

DON’T:

  • ❌ Use functions that prevent index usage
  • ❌ Create expressions with multiple subqueries
  • ❌ Apply RLS to tables with billions of rows without proper indexing

4. Operations

DO:

  • ✅ Apply policies to all relevant operations (SELECT, UPDATE, DELETE)
  • ✅ Test query rewriting in development
  • ✅ Monitor RLS metrics and performance
  • ✅ Document policy changes in changelog

DON’T:

  • ❌ Skip testing after policy modifications
  • ❌ Change policies in production without validation
  • ❌ Delete policies without understanding impact

Policy Patterns

Pattern 1: Basic Tenant Isolation

Use Case: Multi-tenant SaaS with complete data isolation

{
"name": "tenant_isolation",
"table": "users",
"expression": "tenant_id = {tenant_id}",
"operations": ["SELECT", "UPDATE", "DELETE"]
}

Pattern 2: Hierarchical Access

Use Case: Organization with departments and teams

{
"name": "department_access",
"table": "records",
"expression": "tenant_id = {tenant_id} AND department_id IN (SELECT id FROM user_departments WHERE user_id = {user_id})",
"operations": ["SELECT"]
}

Pattern 3: Owner-Based Access

Use Case: Users can only access their own data

{
"name": "owner_access",
"table": "documents",
"expression": "tenant_id = {tenant_id} AND (created_by = {user_id} OR shared_with_user_id = {user_id})",
"operations": ["SELECT", "UPDATE", "DELETE"]
}

Pattern 4: Role-Based Access

Use Case: Admins see all data, users see their own

{
"name": "role_access",
"table": "transactions",
"expression": "tenant_id = {tenant_id} AND ({role} = 'admin' OR user_id = {user_id})",
"operations": ["SELECT"]
}

Pattern 5: Time-Based Access

Use Case: Data visible only within validity period

{
"name": "temporal_access",
"table": "contracts",
"expression": "tenant_id = {tenant_id} AND start_date <= {timestamp} AND end_date >= {timestamp}",
"operations": ["SELECT"]
}

Error Handling

Common Error Codes

CodeDescriptionResolution
400Bad RequestCheck policy validation errors
401UnauthorizedVerify authentication token
403ForbiddenCheck tenant permissions
404Not FoundVerify tenant/policy exists
409ConflictPolicy already exists
429Too Many RequestsReduce request rate
500Internal Server ErrorContact support

Error Response Format

{
"error": "Bad Request",
"message": "SQL expression contains potentially dangerous keyword: DROP",
"code": 400,
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policy_name": "malicious_policy"
}

Validation Error Examples

Example 1: Dangerous SQL Keyword

{
"error": "Bad Request",
"message": "SQL expression contains potentially dangerous keyword: DROP",
"code": 400,
"tenant_id": "550e8400-e29b-41d4-a716-446655440000"
}

Example 2: Empty Expression

{
"error": "Bad Request",
"message": "SQL expression cannot be empty",
"code": 400,
"tenant_id": "550e8400-e29b-41d4-a716-446655440000"
}

Example 3: Invalid Table Name

{
"error": "Bad Request",
"message": "Table name must contain only alphanumeric characters and underscores",
"code": 400,
"tenant_id": "550e8400-e29b-41d4-a716-446655440000"
}

Example 4: Policy Already Exists

{
"error": "Conflict",
"message": "Policy 'tenant_isolation' already exists for table 'users'",
"code": 409,
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"policy_name": "tenant_isolation"
}

Performance Considerations

Index Requirements

For optimal performance, create indexes on columns used in RLS expressions:

-- Index for tenant_id filtering
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Composite index for complex filtering
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- Covering index for common queries
CREATE INDEX idx_documents_tenant_owner_status ON documents(tenant_id, owner_id, status);

Query Performance

Before RLS:

  • Simple query: ~5ms
  • Join query: ~15ms

After RLS (with proper indexes):

  • Simple query: ~7ms (+2ms overhead)
  • Join query: ~18ms (+3ms overhead)

Target: <5% query overhead with properly indexed columns

Monitoring

Monitor these metrics:

  • RLS policy application rate
  • Query rewrite latency
  • Index usage on RLS columns
  • Cross-tenant access attempts (should be 0)

Rate Limits

  • Policy Creation: 10 requests/minute per tenant
  • Policy Listing: 50 requests/minute per tenant
  • Policy Deletion: 10 requests/minute per tenant

Note: Rate limits are subject to change based on tenant tier.


Support

For questions or issues with the RLS Policy API:


Last Updated: 2025-12-09 API Version: v1.0.0