Drizzle ORM

Row Level Security

Roles, policies and enableRLS on PostgreSQL tables, recorded for your migrations.

Row level security is PostgreSQL only. drizzle-kit sees your roles and policies in your migrations, and your row types do not change.

import * as DZ from '@mionjs/drizzle-orm-pg-core';
import {sql} from '@mionjs/drizzle-orm';

export const appUser = DZ.pgRole('app_user', {
  createDb: false,
  createRole: false,
  inherit: true,
});

// a managed role from your provider, say
export const authenticated = DZ.pgRole('authenticated').existing();

export const documents = DZ.pgTable(
  'documents',
  {
    id: DZ.uuid('id', {defaultRandom: true, primaryKey: true}),
    ownerId: DZ.uuid('owner_id', {notNull: true}),
    title: DZ.varchar('title', {length: 200, notNull: true}),
    body: DZ.text('body'),
  },
  (t) => [
    DZ.index('documents_owner_idx').on(t.ownerId),
    DZ.pgPolicy('documents_owner_reads', {
      as: 'permissive',
      for: 'select',
      to: authenticated,
      using: sql`${t.ownerId} = current_setting('app.user_id')::uuid`,
    }),
    DZ.pgPolicy('documents_owner_writes', {
      as: 'permissive',
      for: 'insert',
      to: appUser,
      withCheck: sql`${t.ownerId} = current_setting('app.user_id')::uuid`,
    }),
  ]
).enableRLS();

// needs its own toDrizzle call in the schema file
export const adminReadsEverything = DZ.pgPolicy('documents_admin_reads', {
  for: 'select',
  to: 'postgres',
  using: sql`true`,
}).link(documents);

// @ts-expect-error RLS is already enabled on this table
export const doubleEnabled = documents.enableRLS();

Database Roles

pgRole(name, config) declares a role for drizzle-kit to create. existing() marks a role that is already there, so drizzle-kit leaves it alone:

const appUser = pgRole('app_user', {createRole: true});
const authenticated = pgRole('authenticated').existing();

Security Policies

A policy declared in the table's third argument belongs to that table and comes with it. as, for, to, using and withCheck are Drizzle's own options.

A policy declared apart and attached with link() belongs to no table, so export it and materialize it yourself, or your migrations will not see it:

export const adminReads = pgPolicy('admin_reads', {for: 'select', using: sql`true`}).link(documents);

Enabling Row Level Security

enableRLS() turns row level security on for a table. You can call it only once: the type it returns drops the method, as in Drizzle.

Provider Policy Helpers

Helpers like crudPolicy from drizzle-orm/neon, and the predefined roles in drizzle-orm/supabase, are not wrapped here. Import them from Drizzle and put the result in the array as is:

import {crudPolicy} from 'drizzle-orm/neon';

pgTable('docs', {...}, () => [crudPolicy({role: admin, read: true, modify: false})]);
Copyright © 2026