Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Dev.to
Discord
WhatsApp Channel
daily.dev
Hashnode
X

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /
  3. Tools
  4. /
  5. RLSBuilder
Free · Live

Stop retyping the same RLS policy

Enter a table name and a tenant column, get the exact CREATE POLICY SQL and a Jest test that proves isolation actually works. No signup, nothing leaves your browser.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

How RLSBuilder works

RLSBuilder is a string template, not a database connection. Nothing you type reaches a server.

  1. 1

    You enter a table name and tenant column

    The table name is whatever tenant-scoped table you are securing (invoices, documents, projects). The tenant column is the column that identifies which tenant a row belongs to, usually tenant_id.

  2. 2

    You pick a tenant ID type and isolation strategy

    Tenant ID type (uuid, integer, or text) controls the cast applied when comparing the column against the session variable. Isolation strategy decides whether tenancy is enforced through a session variable your application sets, or through a PostgreSQL role that maps one-to-one with a tenant.

  3. 3

    You select which operations need a policy

    SELECT, INSERT, UPDATE, and DELETE are checked by default. Uncheck any your table does not need, for example a table that is insert-only from the application and never updated in place.

  4. 4

    The SQL panel renders on every keystroke

    ALTER TABLE ... ENABLE ROW LEVEL SECURITY and FORCE ROW LEVEL SECURITY come first, followed by one CREATE POLICY per selected operation. UPDATE always gets both a USING and a WITH CHECK clause, the split most hand-written policies get wrong.

  5. 5

    The Jest panel renders alongside it

    For every selected operation, a matching test asserts that a second tenant cannot read, write, or destroy the first tenant's row. A bonus regression test checks that your application role does not have BYPASSRLS.

  6. 6

    You copy both panels into your project

    Paste the SQL into a migration, and the test below your existing Jest setup. Fill in the TODO comments (your seed helper and a real test database connection) and run it in CI.

What each generated policy does

Four operations, four different failure modes if a policy is missing or incomplete. Here is what each one actually blocks, and the mistake it exists to catch.

SELECT

Filters which existing rows a query can return. Without it, any authenticated connection sees every tenant's rows in a plain SELECT * FROM table.

-- Tenant B runs this and gets zero rows back, not an error
SELECT * FROM invoices WHERE tenant_id = 'tenant-a';
INSERT

Uses WITH CHECK, not USING, because there is no existing row to filter yet, only the row about to be created. Without it, tenant B can insert a row and simply label it as tenant A's.

-- Rejected with a row-level security error, not silently ignored
INSERT INTO invoices (tenant_id, amount) VALUES ('tenant-a', 500);
UPDATE

The only operation that needs both clauses. USING controls which existing rows can be targeted; WITH CHECK controls what the row is allowed to become. A policy with only USING lets a tenant update their own row into someone else's tenant_id.

-- Blocked by WITH CHECK even though USING allowed targeting the row
UPDATE invoices SET tenant_id = 'tenant-a' WHERE tenant_id = 'tenant-b';
DELETE

Filters which rows a DELETE statement can remove. Without it, a broad DELETE FROM invoices WHERE amount > 0 run by any tenant clears every tenant's data, not just its own.

-- Only rows matching the caller's own tenant are ever deleted
DELETE FROM invoices WHERE amount > 0;

Policy SQL syntax reference

Every generated policy follows the same shape. Knowing the parts helps you edit the output by hand once your policy needs logic beyond a single tenant column.

Anatomy of a generated policy
CREATE POLICY invoices_update_tenant_isolation ON invoices
  FOR UPDATE
  USING (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
--     |                |                                     |
--     |                |                                     +-- cast matches your chosen tenant ID type
--     |                +-- current_setting reads the session variable your app sets per request
--     +-- USING: which existing rows this policy applies to
--         WITH CHECK: what a resulting row is allowed to look like
Session variable strategy: setting the tenant per request
-- Inside a transaction, scoped to that transaction only
SET LOCAL app.tenant_id = '3f1b2c4a-...';

-- current_setting() with the second argument true returns NULL
-- instead of erroring if the variable was never set
SELECT current_setting('app.tenant_id', true);
Edge cases worth knowing
-- FORCE is required or the table owner bypasses every policy above
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;

-- A cast mismatch fails at query time, not at CREATE POLICY time
-- if tenant_id is integer but the session variable holds a UUID string
tenant_id = current_setting('app.tenant_id')::integer   -- errors on a UUID value

-- Superusers ALWAYS bypass RLS, FORCE included. Never test as postgres.

When to use RLSBuilder

Each row maps a real situation to what to type into the tool.

SituationWhat to enterWhat to check
Adding RLS to a brand new tenant-scoped tableThe new table's name and its tenant_id columnAll four operations, unless the table is truly read-only from the app
Auditing an existing policy that only has USINGThe table, plus UPDATE and INSERT specificallyCompare the generated WITH CHECK clause against what is actually deployed
Writing the Jest regression suite before a PR reviewEvery tenant-scoped table touched in the PRThe generated test file, paste-ready into your existing suite
Security review comparing policies against a baselineEach table under review, one at a timeWhether the deployed policy matches what a correct one should look like
Deciding between session variable and role-per-tenantThe same table with both strategies selected in turnHow the SQL and test shape differ, before committing to one

Frequently Asked Questions

What does RLSBuilder do?

RLSBuilder generates PostgreSQL Row-Level Security policy SQL and a matching Jest test file from three inputs: a table name, a tenant/scope column, and the operations you want covered (SELECT, INSERT, UPDATE, DELETE). It outputs the exact CREATE POLICY statements with correctly split USING and WITH CHECK clauses, plus a ready-to-paste Jest suite that proves tenant isolation actually holds, not just that the SQL is syntactically valid.

Everything runs client-side. There is no database connection, no account, and no server round-trip.

How is this different from writing the policy by hand?
RLSBuilderWriting it by hand
Time per tableUnder a minute5-15 minutes, more if you forget WITH CHECK
WITH CHECK on UPDATEAlways includedFrequently forgotten, the most common RLS bug
Matching Jest testGenerated alongside the SQLUsually skipped or written later, if ever
Consistency across tablesSame shape every timeDrifts as different people copy-paste old policies

The SQL itself is not hard to write once. The value is in not retyping the same boilerplate for the fifteenth tenant-scoped table, and in never shipping a policy without the test that proves it works.

How is this different from an AI SQL generator?

General-purpose AI SQL generators (ChatGPT, AI2sql, and similar) produce policy SQL from a prompt, but output is inconsistent between runs, usually requires an account, and rarely generates a matching test in the same shape twice.

RLSBuilder is a fixed template, not a prompt. The same three inputs always produce the same SQL and the same test structure, which matters when you are generating policies for a dozen tables and want them to look like they came from the same hand.

Does RLSBuilder send my table or schema anywhere?

No. The table name, tenant column, and selected operations are string-templated into SQL and JavaScript entirely in your browser. Nothing is sent to a server, logged, or stored. You can use real internal table and column names without exposing your schema to a third party.

How do I use the generated Jest test in my own suite?

Paste the generated test below your own test setup file, then fill in the two TODO comments: a seed helper that inserts a row for the row-level fixture, and a testPool / adminPool connection to a real PostgreSQL instance (a postgres service container in CI works well). The generated test assumes a queryAsTenant helper, which is included at the top of the output.

js
test('tenant B cannot see tenant A rows', async () => {
  // TODO: seed a row with tenant_id = 'tenant-a' using your own seed helper
  const rows = await queryAsTenant(
    testPool,
    'tenant-b',
    'SELECT * FROM invoices WHERE tenant_id = $1',
    ['tenant-a']
  );

  expect(rows).toHaveLength(0);
});

For the full walkthrough of why this pattern catches bugs that a happy-path test misses, see the paired guide on testing PostgreSQL RLS with Jest linked below.

Should I use the session variable or role-per-tenant strategy?

Session variable (SET LOCAL app.tenant_id) is the more common pattern for application backends: one shared database role, with the application setting the tenant on every connection or transaction. It is what most NestJS, Express, and Fastify setups already use, and it is the strategy the paired blog post walks through in depth.

Role-per-tenant (matching against current_user) fits setups where PostgreSQL roles map directly to tenants, such as a database-per-customer-role architecture. It needs one login role per tenant to exist ahead of time, which the generated SQL leaves as a comment rather than assuming for you.

Does the generated SQL include FORCE ROW LEVEL SECURITY?

Yes, every generation includes ALTER TABLE ... FORCE ROW LEVEL SECURITY alongside ENABLE ROW LEVEL SECURITY. Without FORCE, the table owner and any superuser connection bypass every policy by default, which is the single most common reason a manually written RLS test passes even though the policy is broken.

Related reading

Guide

Testing PostgreSQL RLS Policies With Jest (No pgTAP)

The full pattern behind the generated test: proving a leak, testing WITH CHECK, and catching an accidental BYPASSRLS grant.

Tool

UpsertPicker

Choosing between ON CONFLICT DO NOTHING, DO UPDATE, and PostgreSQL 19's DO SELECT for the same tenant-scoped tables.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.