Skip to main content

kysera rls

Row-Level Security utilities for native PostgreSQL policies. When you define policies with defineRLSSchema from @kysera/rls, the plugin can enforce them in the ORM layer — but PostgreSQL can also enforce them in the database itself with CREATE POLICY. These commands translate your RLS schema module into that native SQL: rls generate prints (or writes) the raw statements, rls migration wraps them in a ready-to-run Kysely migration.

Neither command connects to the database — they only load your configuration (to confirm the dialect is PostgreSQL) and your schema module.

Quick Reference

CommandDescription
rls generate <schema-module>Generate native PostgreSQL RLS statements from an RLS schema module
rls migration <schema-module>Generate a Kysely migration file applying the policies

:::warning PostgreSQL only Native RLS targets PostgreSQL's row security features. With any other configured dialect both commands fail up front (RLS_DIALECT_ERROR). For MySQL and SQLite, use the ORM-level rlsPlugin() from @kysera/rls instead. :::

The Schema Module

Both commands take a path to a module that default-exports the result of defineRLSSchema(...) (named exports rlsSchema or schema are accepted as fallbacks). Only policies carrying native SQL in using and/or withCheck become CREATE POLICY statements — ORM-only policies (filter/validate, or function conditions without SQL) are skipped with a note.

rls/schema.ts
import { defineRLSSchema } from '@kysera/rls'

export default defineRLSSchema({
posts: {
policies: [
{
type: 'allow',
operation: 'read',
role: 'app_user',
condition: '', // ORM-side rule; empty string for native-only policies
using: "tenant_id = current_setting('app.tenant_id')::uuid"
},
{
type: 'allow',
operation: 'create',
role: 'app_user',
condition: '',
withCheck: "tenant_id = current_setting('app.tenant_id')::uuid"
}
]
}
})

:::tip condition is required defineRLSSchema validates every policy and requires a condition (the rule the ORM plugin evaluates — a function or string). For policies that only exist as native SQL, pass condition: '' and put the SQL in using/withCheck. :::

Module loading rules:

  • Compiled .js/.mjs/.cjs modules always load.
  • .ts/.mts modules load when the runtime can import TypeScript — Node.js >= 22.18 (erasable-syntax type stripping) or Bun. Since the CLI itself requires Node >= 22.18, plain type-annotated modules work out of the box; modules using non-erasable syntax (enums, namespaces) must be compiled first.
  • The module's own imports must resolve — @kysera/rls has to be installed in your project.

rls generate

Print the native SQL to stdout, or write it to a file.

kysera rls generate ./rls/schema.ts

Options

OptionDescription
-o, --output <file>Write SQL to a file instead of stdout
--dropGenerate DROP/DISABLE statements instead of CREATE/ENABLE
--functionsPrepend the RLS context helper functions (rls_current_user_id, ...)
-s, --schema <name>PostgreSQL schema name (default: public)
--policy-prefix <prefix>Prefix for generated policy names (default: rls)
--no-forceSkip FORCE ROW LEVEL SECURITY (table owners bypass RLS)
--jsonOutput { statements: [...] } as JSON
-c, --config <path>Path to configuration file

Example

$ kysera rls generate ./rls/schema.ts
ALTER TABLE "public"."posts" ENABLE ROW LEVEL SECURITY;

ALTER TABLE "public"."posts" FORCE ROW LEVEL SECURITY;

CREATE POLICY "rls_posts_allow_0"
ON "public"."posts"
AS PERMISSIVE
TO "app_user"
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::uuid);

CREATE POLICY "rls_posts_allow_1"
ON "public"."posts"
AS PERMISSIVE
TO "app_user"
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

Policy names follow <prefix>_<table>_<type>_<index> unless a policy sets its own name.

Rolling back is symmetric — --drop emits a DO $$ block that drops every policy matching the prefix on each table, then disables row security:

kysera rls generate ./rls/schema.ts --drop -o drop-rls.sql

--functions prepends the STABLE SQL helper functions the policies can build on (rls_current_user_id(), rls_current_tenant_id(), rls_current_roles(), rls_has_role(), rls_current_permissions(), rls_has_permission()), each reading an app.* session variable. It cannot be combined with --drop.

With -o/--output, the target directory is created as needed and a summary goes to stdout; global --dry-run reports what would be written without touching the file.

:::warning FORCE is on by default Generated SQL includes FORCE ROW LEVEL SECURITY, so even the table owner is subject to the policies. Pass --no-force if your application connects as the owning role and must bypass RLS — and understand what that means for enforcement. :::

rls migration

Generate a Kysely migration file instead of raw SQL — the same statements wrapped in up()/down(), written into your migrations directory alongside your other migrations.

kysera rls migration ./rls/schema.ts

Options

OptionDescription
-d, --dir <path>Migrations directory (default: from configuration)
-n, --name <name>Migration name (default: rls_policies)
-s, --schema <name>PostgreSQL schema name (default: public)
--policy-prefix <prefix>Prefix for generated policy names (default: rls)
--no-forceSkip FORCE ROW LEVEL SECURITY (table owners bypass RLS)
--no-functionsOmit the RLS context helper functions from the migration
-c, --config <path>Path to configuration file

Example

$ kysera rls migration ./rls/schema.ts -n enable_rls
Migration created: 20260802140351_enable_rls.ts
/work/my-app/migrations/20260802140351_enable_rls.ts
Run 'kysera migrate up' to apply the RLS policies

The file is named <timestamp>_<name>.ts, matching kysera migrate create filenames, so kysera migrate up picks it up in order. Its up() creates the context functions (unless --no-functions), enables row security, and creates the policies; down() drops them again. If a file with the same name already exists the command fails rather than overwriting; global --dry-run previews without writing.

Note the defaults differ deliberately: rls generate omits the helper functions unless you pass --functions, while rls migration includes them unless you pass --no-functions — a migration should be self-contained.

Typical Workflow

# 1. Define policies once, in code
# (rls/schema.ts default-exporting defineRLSSchema({...}))

# 2. Emit a migration and apply it
kysera rls migration ./rls/schema.ts -n enable_rls
kysera migrate up

# 3. In the application, set the session variables the policies read
# (app.tenant_id etc.) — the @kysera/rls plugin does this for you
# when configured with the same schema.

See Also

  • @kysera/rls plugin — ORM-level enforcement of the same schema, and how session context is set
  • migrate — applying the generated migration
  • schema — multi-tenant schema-per-tenant management, an alternative isolation strategy