Simplify RLS with Drizzle
Use Drizzle crudPolicy to manage Row-Level Security with Neon Authorize
What you'll learn
How to simplify Row-Level Security using
crudPolicy
Common RLS patterns with Drizzle
Related docs
Why simplify RLS policies?
Row-Level Security (RLS) is an important last line of defense for protecting your data at the database level. However, implementing RLS requires writing and maintaining separate SQL policies for each CRUD operation (Create, Read, Update, Delete), which can be both tedious and error-prone.
For example
To illustrate, let's consider a simple Todo list app with RLS policies applied to a todos
table. Postgres RLS policies use two types of conditions:
USING
clause — controls which existing rows can be accessedWITH CHECK
clause — controls what new or modified data can be written
note
auth.user_id()
and the role it plays in these policies, see this explanation.Here's how these clauses apply to each operation:
Operation | USING clause | WITH CHECK clause |
---|---|---|
Select | auth.user_id() = user_id | |
Insert | auth.user_id() = user_id | |
Update | auth.user_id() = user_id | auth.user_id() = user_id |
Delete | auth.user_id() = user_id |
And the SQL code would look like this:
CREATE TABLE IF NOT EXISTS "todos" (
"id" bigint PRIMARY KEY,
"user_id" text DEFAULT (auth.user_id()) NOT NULL,
"task" text NOT NULL,
"is_complete" boolean DEFAULT false NOT NULL,
"inserted_at" timestamp with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "todos" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "create todos" ON "todos" AS PERMISSIVE FOR INSERT TO "authenticated" WITH CHECK ((select auth.user_id() = user_id));
CREATE POLICY "view todos" ON "todos" AS PERMISSIVE FOR SELECT TO "authenticated" USING ((select auth.user_id() = user_id));
CREATE POLICY "crud-authenticated-policy-update" ON "todos" AS PERMISSIVE FOR UPDATE TO "authenticated" USING ((select auth.user_id() = "todos"."user_id")) WITH CHECK ((select auth.user_id() = "todos"."user_id"));
CREATE POLICY "delete todos" ON "todos" AS PERMISSIVE FOR DELETE TO "authenticated" USING ((select auth.user_id() = user_id));
As you add new features, you'll need to add more policies to match. This growing complexity can lead to subtle bugs that can be hard to spot in a large schema file filled with SQL statements.
Simplifying RLS with crudPolicy
The crudPolicy
function generates RLS policies by accepting a simple configuration object. Let's break down its usage:
import { crudPolicy, authenticatedRole, authUid } from 'drizzle-orm/neon';
// Define a table with RLS policies
export const todos = pgTable(
'todos',
{
id: bigint().primaryKey(),
userId: text()
.notNull()
.default(sql`(auth.user_id())`),
task: text().notNull(),
isComplete: boolean().notNull().default(false),
},
(table) => [
// Apply RLS policy
crudPolicy({
role: authenticatedRole,
read: authUid(table.userId),
modify: authUid(table.userId),
}),
]
);
Configuration parameters
The crudPolicy
function accepts these parameters:
role
: The Postgres role(s) to apply the policy to. Can be a single role or an array of rolesread
: Controls SELECT operations:true
to allow all readsfalse
to deny all reads- A custom SQL expression
null
to prevent policy generation
modify
: Controls INSERT, UPDATE, and DELETE operations:true
to allow all modificationsfalse
to deny all modifications- A custom SQL expression
null
to prevent policy generation
When executed, crudPolicy
generates an array of RLS policy definitions covering all CRUD operations (select, insert, update, delete).
The authUid Helper
For user-specific policies, Drizzle provides the authUid
helper function:
export const authUid = (userIdColumn: AnyPgColumn) =>
sql`(select auth.user_id() = ${userIdColumn})`;
This helper:
- Wraps Neon Authorize's
auth.user_id()
function (from the pg_session_jwt extension) - Compares the authenticated user's ID with a table column
- Returns a SQL expression suitable for use in
read
andmodify
parameters
Common patterns
Now that we understand how crudPolicy
works, let's look at two typical ways to secure your tables:
Basic access control
The most common pattern is restricting users to their own data:
import { crudPolicy, authenticatedRole, authUid } from 'drizzle-orm/neon';
export const todos = pgTable(
'todos',
{
id: bigint().primaryKey(),
userId: text()
.notNull()
.default(sql`(auth.user_id())`),
task: text().notNull(),
isComplete: boolean().notNull().default(false),
insertedAt: timestamp({ withTimezone: true }).defaultNow().notNull(),
},
(table) => [
crudPolicy({
role: authenticatedRole,
read: authUid(table.userId), // users can only read their own todos
modify: authUid(table.userId), // users can only modify their own todos
}),
]
);
Role-based access control
For more complex scenarios, you might want different permissions for different roles:
import { crudPolicy, authenticatedRole, anonymousRole } from 'drizzle-orm/neon';
export const posts = pgTable(
'posts',
{
id: bigint().primaryKey(),
userId: text()
.notNull()
.default(sql`(auth.user_id())`),
content: text().notNull(),
published: boolean().notNull().default(false),
},
(table) => [
// Public read access
crudPolicy({
role: anonymousRole,
read: true, // anyone can read posts
modify: false, // no modifications allowed
}),
// Authenticated user access
crudPolicy({
role: authenticatedRole,
read: true, // can read all posts
modify: authUid(table.userId), // can only modify own posts
}),
]
);
Example application
Check out our social wall sample application, a simple schema that demonstrates RLS policies with crudPolicy
. It implements a social wall where:
- Anyone can view the wall
- Authenticated users can modify their own posts
Need help?
Join our Discord Server to ask questions or see what others are doing with Neon. Users on paid plans can open a support ticket from the console. For more details, see Getting Support.