What is RLS?
Row Level Security (RLS) allows you to control which rows a user can access based on their identity. Instead of manually checking permissions in every reducer, you declare policies once on the table definition.
Security-by-Default
With RLS, developers cannot accidentally forget authorization checks. The database enforces policies automatically on every query and subscription.
Basic Syntax
Rust
#[table(name = "documents", public)] #[rls(read = "owner_id = ctx.sender()")] #[rls(write = "owner_id = ctx.sender()")] pub struct Document { #[primary_key] pub id: u64, #[index] pub owner_id: Identity, pub title: String, pub content: String, }
The #[rls] attribute accepts SQL predicates that reference:
ctx.sender()— The authenticated identity making the requestctx.timestamp()— Current server timestamp- Column names from the table
- Subqueries against other tables
Policy Types
Read Policy
Applied to all SELECT queries and subscriptions:
Rust
#[rls(read = "is_public = true OR owner_id = ctx.sender()")]
Write Policy
Applied to INSERT, UPDATE, DELETE operations:
Rust
#[rls(write = "owner_id = ctx.sender() OR ctx.sender() IN (SELECT admin_id FROM admins)")]
Common Patterns
User-Owned Data
Rust
#[table(name = "todos")] #[rls(read = "user_id = ctx.sender()")] #[rls(write = "user_id = ctx.sender()")] pub struct Todo { #[primary_key] pub id: u64, #[index] pub user_id: Identity, pub text: String, pub completed: bool, }
Multi-Tenant (Organization-Scoped)
Rust
#[table(name = "projects")] #[rls(read = "org_id IN (SELECT org_id FROM memberships WHERE user_id = ctx.sender())")] pub struct Project { #[primary_key] pub id: u64, #[index] pub org_id: u64, pub name: String, }
Public Read, Owner Write
Rust
#[table(name = "blog_posts", public)] #[rls(read = "published = true OR author_id = ctx.sender()")] #[rls(write = "author_id = ctx.sender()")] pub struct BlogPost { #[primary_key] pub id: u64, #[index] pub author_id: Identity, pub title: String, pub content: String, pub published: bool, }
How It Works
When a client subscribes or queries data:
- The RLS predicate is automatically injected as a WHERE clause
- The query planner uses indexes efficiently (policies should reference indexed columns)
- Only matching rows are returned to the client
- Updates are filtered through the write policy before being applied
SQL
-- Client subscribes to: SELECT * FROM todos; -- Server actually executes: SELECT * FROM todos WHERE user_id = 'current-user-identity';
Performance Tips
- Index policy columns: Always index columns used in RLS predicates (owner_id, org_id, etc.)
- Cache policies: Policies are cached per-identity to avoid re-parsing
- Avoid complex subqueries: Subqueries in policies are evaluated per-row—use sparingly
- Test with EXPLAIN: Use query EXPLAIN to verify indexes are used
Comparison with Alternatives
| Feature | PostgreSQL RLS | Firebase Rules | Cosmictron RLS |
|---|---|---|---|
| Syntax | SQL functions | JSON DSL | SQL predicates |
| Compile-time safety | Runtime errors | Rules simulator | Type-checked |
| Subqueries | Yes | Limited | Yes |