Rowguard - RLS Policy DSL - v0.2.0
    Preparing search index...

    Rowguard - RLS Policy DSL - v0.2.0

    Rowguard - RLS Policy DSL

    npm version Docs License: MIT pkg.pr.new

    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. Use with caution in production.

    ⚠️ No Performance Evaluation: This library does not evaluate policy performance. You should use Supabase Performance Advisor to evaluate your RLS policy performance.

    Try the live demo at https://rowguard-demo.vercel.app/

    The demo now includes live database testing using the real Supabase migration workflow:

    • 📁 Save as Migration Files - Generate timestamped migration files from your policies
    • 🔄 Apply with Supabase CLI - Use standard supabase db reset to apply migrations
    • 📊 Browse Database Schema - View all tables and columns from your local instance
    • 👥 Test as Different Users - Sign in as test users to verify RLS enforcement
    • Verify in Real-Time - See exactly which rows each user can access with RLS active

    This teaches you the real Supabase development workflow - the same way you'll deploy policies to production!

    Run the full demo locally with database:

    pnpm install
    pnpm demo:dev:full # Starts Supabase + demo

    Or run in SQL-only mode (no database):

    pnpm demo:dev
    

    The demo source code is in the demo/ directory. See demo/README.md for detailed setup instructions.

    • Type-safe schema integration - Autocomplete and compile-time validation with Supabase-generated types
    • 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

    Install via npm:

    npm install rowguard
    
    # pnpm
    pnpm add rowguard

    # yarn
    yarn add rowguard

    # bun
    bun add rowguard

    Preview builds are available via pkg-pr-new for each PR/commit:

    # Install a preview build from a PR (check pkg.pr.new badge for latest URL)
    npm install https://pkg.pr.new/supabase-community/rowguard@{pr-number}

    Get autocomplete and compile-time validation by generating types from your database schema.

    # For remote project
    npx supabase gen types typescript --project-id "$PROJECT_REF" > database.types.ts

    # For local development
    npx supabase gen types typescript --local > database.types.ts
    import { createRowguard } from 'rowguard';
    import { Database } from './database.types';

    const rg = createRowguard<Database>();

    // ✅ Autocomplete for tables and columns
    const userDocsPolicy = rg
    .policy('user_documents')
    .on('documents') // ← IDE shows all table names
    .read()
    .when(rg.column('documents', 'user_id').eq(rg.auth.uid()));
    // ↑ autocomplete ↑ autocomplete columns

    // ❌ Type errors caught at compile time
    // rg.column('documents', 'nonexistent_column') // TypeScript error
    // rg.column('documents', 'user_id').eq(42) // Type error: string !== number

    console.log(userDocsPolicy.toSQL());

    Benefits:

    • ✅ Autocomplete for tables and columns
    • ✅ Catch typos at compile time
    • ✅ Type-safe value comparisons
    • ✅ Safe refactoring

    If you don't have a Supabase project or prefer not to generate types:

    import { policy, column, auth, from, session } from 'rowguard';

    // 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());

    ⚠️ Note: Without generated types, you won't get autocomplete or compile-time validation.

    import { policies } from 'rowguard';

    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 'rowguard';

    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 user
    policies.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 UPDATE

    We welcome contributions! Please see our Contributing Guide for details on:

    • Setting up your development environment
    • Running tests and building the project
    • Code style guidelines
    • How to submit pull requests
    • Testing your changes with preview deployments
    # Install dependencies
    pnpm install

    # Build the library
    pnpm run build

    # Run tests
    pnpm test

    # Run integration tests (requires Supabase CLI)
    pnpm run test:integration:full

    # Run the interactive demo
    pnpm run demo:dev

    For more detailed information, see CONTRIBUTING.md.

    This project uses automated releases via release-please.

    • All commits must follow Conventional Commits format
    • Releases are automatically published to npm when maintainers merge the release PR
    • For detailed information, see RELEASE.md

    MIT - see LICENSE file for details