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 request
  • ctx.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:

  1. The RLS predicate is automatically injected as a WHERE clause
  2. The query planner uses indexes efficiently (policies should reference indexed columns)
  3. Only matching rows are returned to the client
  4. 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

Secure Your App

Build with confidence using declarative security.

Full Documentation → See Chat Example