A TypeScript DSL for defining PostgreSQL Row Level Security (RLS) policies with a clean, type-safe API.
⚠️ Experimental: This is an experimental project and not an official Supabase library. It is not published to npm. Test builds are available via pkg-pr-new for evaluation purposes only.
- Simple & intuitive fluent API that reads like natural language
- Natural left-to-right method chaining (no polish notation)
- Zero dependencies - pure TypeScript, works everywhere
- Full TypeScript support with intelligent inference
- Universal - Node.js, Deno, Bun, browsers, edge functions
- Minimal footprint, tree-shakeable
This package is not published to npm. Test builds are available via pkg-pr-new for each PR/commit:
# Install a specific commit build (check pkg.pr.new badge for latest URL)
npm install https://pkg.pr.new/supabase/ts-to-rls@{commit-sha}import { policy, column, auth, from, session } from 'ts-to-rls';
// Simple user ownership (using user-focused API)
const userDocsPolicy = policy('user_documents')
.on('documents')
.read()
.when(column('user_id').eq(auth.uid()));
// Complex conditions with method chaining
const complexPolicy = policy('project_access')
.on('projects')
.read()
.when(
column('is_public').eq(true)
.or(column('user_id').eq(auth.uid()))
.or(column('organization_id').eq(session.get('app.org_id', 'uuid')))
);
// Subqueries
const memberPolicy = policy('member_access')
.on('projects')
.read()
.when(
column('id').in(
from('project_members')
.select('project_id')
.where(column('user_id').eq(auth.uid()))
)
);
console.log(userDocsPolicy.toSQL());import { policies } from 'ts-to-rls';
const [policy] = policies.userOwned('documents', 'SELECT');
const tenantPolicy = policies.tenantIsolation('tenant_data');
const publicPolicy = policies.publicAccess('projects');This library provides two API styles:
User-Focused API (Recommended) - Uses intuitive terms like read(), write(), update(), requireAll():
policy('user_docs')
.on('documents')
.read() // Instead of .for('SELECT')
.requireAll() // Instead of .restrictive()
.when(column('user_id').isOwner());RLS-Focused API - Uses PostgreSQL RLS terminology like for('SELECT'), restrictive():
policy('user_docs')
.on('documents')
.for('SELECT') // RLS terminology
.restrictive() // RLS terminology
.when(column('user_id').isOwner());Both APIs are fully supported and produce identical SQL. The user-focused API is recommended for better readability and developer experience.
policy(name)
.on(table) // Target table
.read() // User-focused: allow reading (SELECT)
.write() // User-focused: allow creating (INSERT)
.update() // User-focused: allow updating (UPDATE)
.delete() // User-focused: allow deleting (DELETE)
.all() // User-focused: allow all operations (ALL)
// Or use RLS-focused API:
.for(operation) // SELECT | INSERT | UPDATE | DELETE | ALL
.to(role?) // Optional role restriction
.when(condition) // USING clause (read filter)
.allow(condition) // Type-safe USING/WITH CHECK based on operation
.withCheck(condition) // WITH CHECK clause (write validation)
.requireAll() // User-focused: all policies must pass (RESTRICTIVE)
.allowAny() // User-focused: any policy can grant access (PERMISSIVE, default)
// Or use RLS-focused API:
.restrictive() // Mark as RESTRICTIVE
.permissive() // Mark as PERMISSIVE (default)
.description(text) // Add documentation
.toSQL() // Generate PostgreSQL statement// Comparisons
column('status').eq('active')
column('age').gt(18)
column('price').lte(100)
// Pattern matching
column('email').like('%@company.com')
column('name').ilike('john%')
// Membership
column('status').in(['active', 'pending'])
column('tags').contains(['important'])
// Null checks
column('deleted_at').isNull()
column('verified_at').isNotNull()
// Helpers
column('user_id').isOwner() // eq(auth.uid())
column('is_public').isPublic() // eq(true)
// Chaining
column('user_id').eq(auth.uid())
.or(column('is_public').eq(true))
.and(column('status').eq('active'))import { column, from, auth } from 'ts-to-rls';
column('id').in(
from('project_members')
.select('project_id')
.where(column('user_id').eq(auth.uid()))
)
// With joins
column('id').in(
from('projects', 'p')
.select('p.id')
.join('members', column('m.project_id').eq('p.id'), 'inner', 'm')
.where(column('m.user_id').eq(auth.uid()))
)auth.uid() // Current authenticated user
session.get(key, type) // Type-safe session variable
currentUser() // Current database userpolicies.userOwned(table, operations?)
policies.tenantIsolation(table, tenantColumn?, sessionKey?)
policies.publicAccess(table, visibilityColumn?)
policies.roleAccess(table, role, operations?)Automatically generate indexes for RLS performance optimization:
// User-focused API (recommended)
const userDocsPolicy = policy('user_documents')
.on('documents')
.read()
.when(column('user_id').eq(auth.uid()));
const sql = userDocsPolicy.toSQL({ includeIndexes: true });Indexes are created for columns in equality comparisons, IN clauses, and subquery conditions.
// User-focused API (recommended)
policy('user_documents')
.on('documents')
.read()
.when(column('user_id').eq(auth.uid()));
// Or using .allow() for automatic USING/WITH CHECK handling
policy('user_documents')
.on('documents')
.read()
.allow(column('user_id').isOwner());// User-focused API (recommended)
policy('tenant_isolation')
.on('tenant_data')
.all()
.requireAll()
.when(column('tenant_id').belongsToTenant());// User-focused API (recommended)
policy('project_access')
.on('projects')
.read()
.when(
column('user_id').eq(auth.uid())
.or(
column('id').in(
from('project_members')
.select('project_id')
.where(column('user_id').eq(auth.uid()))
)
)
);// User-focused API (recommended)
policy('user_documents_insert')
.on('user_documents')
.write()
.allow(column('user_id').eq(auth.uid()));// User-focused API (recommended)
policy('user_documents_update')
.on('user_documents')
.update()
.allow(column('user_id').eq(auth.uid()));
// .allow() automatically sets both USING and WITH CHECK for UPDATEpnpm install
pnpm run build
pnpm testMIT