drizzle-team / drizzle-team/drizzle-orm
[FEATURE]: Add .forceRLS() to pgTable builder
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Feature hasn't been suggested before.
- [x] I have verified this feature I'm about to request hasn't been suggested before.
### Describe the enhancement you want to request
Drizzle's pgTable(...).enableRLS() emits ALTER TABLE x ENABLE ROW LEVEL SECURITY, but there's no companion method for ALTER TABLE x FORCE ROW LEVEL SECURITY. To get FORCE today, a Drizzle user has to maintain a raw-SQL migration alongside schema.ts, which loses type-safety and diff-aware migration generation for that statement.
Why FORCE matters
Without FORCE ROW LEVEL SECURITY, the table owner role (which is commonly the role the migration runner connects as) bypasses every policy on the table. Postgres docs https://www.postgresql.org/docs/current/ddl-rowsecurity.html:
> Table owners and table superusers normally bypass the row security system. ... To prevent the table owner from bypassing the row security system on his/her tables, the FORCE ROW LEVEL SECURITY option must be enabled.
So a Drizzle migration that does ENABLE ROW LEVEL SECURITY + a set of CREATE POLICY statements looks armed to a casual reviewer, but any session that connects as the table owner (typical for backend cron jobs, admin scripts, or migration runners) sees every row regardless of policies. FORCE closes that escape hatch.
In a recent audit I ran across the Postgres + Drizzle/Prisma/PostGraphile ecosystem, missing FORCE ROW LEVEL SECURITY was the single most common RLS gap — including in every example schema shipped by major hosted-Postgres vendors that use Drizzle.
Proposed API
```
export const posts = pgTable("posts", { /* columns */ }, (t) => [
pgPolicy("posts_select", { /* ... */ }),
])
.enableRLS()
.forceRLS(); // ← new
```
.forceRLS() is independent of .enableRLS() semantically (FORCE only takes effect once RLS is enabled), but mechanically both modifiers compose on the same builder. A call to .forceRLS() without .enableRLS() could either implicitly enable RLS or surface a TS error — both are reasonable; I'd lean toward implicit enable for ergonomics.
Emitted SQL
```
ALTER TABLE "posts" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "posts" FORCE ROW LEVEL SECURITY; -- ← new line in the migration
```
drizzle-kit's diff would treat FORCE the same way it treats ENABLE — emit the matching ALTER TABLE on table creation and on subsequent toggles.
Benefits
1. Closes the "owner bypasses RLS" gap that's invisible to typical reviewers but caught by Postgres static analyzers.
2. Keeps RLS configuration inside schema.ts — no parallel raw-SQL migrations for what is conceptually a one-flag toggle.
3. Small API addition. The existing table-builder modifier infrastructure (enableRLS()) already does the structural lifting; this would mirror that path.
Happy to send a PR if there's interest. Could you let me know the preferred shape — .forceRLS() on the same PgTableWithColumns modifier chain as .enableRLS(), or a config-object variant like pgTable(name, cols, builder, { rls: { enable: true, force: true } })?
Contributor guide
Assessment
This issue has not been assessed yet.