agentsclimarketplace

Postgres rls pattern

Skill jacob-balslev/skill-graph/examples/projects/saas-stripe-postgres/skills/postgres-rls-pattern

Use when writing or reviewing Postgres queries in a multi-tenant SaaS where every table row must be scoped to a single organization. Enforces the FORCE ROW LEVEL SECURITY + USING + WITH CHECK triple on every tenant-bound table, and wraps application queries in an `orgQuery(orgId)` helper that sets `app.current_org_id` before each statement. Do NOT use for cross-org system queries such as billing cron jobs or admin panels (those bypass RLS intentionally via the service role); use a service-role query wrapper instead.From its SKILL.md

Install
npx -y skills add jacob-balslev/skill-graph --skill postgres-rls-pattern

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

10.4 KB, ~1.2k tokens by cl100k_base, as published. Nobody here has run it

Postgres RLS Pattern

Concept of the skill

What it is: The database-enforced tenant isolation pattern for Postgres tables in a multi-organization SaaS. Mental model: The application sets the current organization; Postgres enforces which rows that organization can read or write. Why it exists: A missed WHERE org_id = ... clause should not become a cross-tenant data leak. What it is NOT: It is not a service-role migration pattern, admin reporting bypass, or generic SQL optimization guidance. Adjacent concepts: Row-level policies, session variables, service-role isolation, tenant-bound tables. One-line analogy: It is a database lock that opens only for the current organization. Common misconception: Application-level filters are equivalent to RLS; RLS moves the guardrail into the database itself.

Coverage

  • The three-part policy triple — FORCE ROW LEVEL SECURITY, USING (org_id = current_setting('app.current_org_id')::uuid), and WITH CHECK (org_id = current_setting('app.current_org_id')::uuid) — and why omitting any one part leaves a gap
  • The orgQuery(orgId) application wrapper — a single function that opens a transaction, sets app.current_org_id, runs the caller's query, and commits; why setting the variable once at session start is unsafe under connection pooling
  • Service role bypass — legitimate cross-org operations (billing cron, admin panel, migration backfills) that must use a connection string that skips RLS, and why those code paths must be isolated from application code
  • Policy audit checklist — grepping for query() calls without a preceding SET app.current_org_id as a CI-safe audit gate
  • New-table checklist — steps to add RLS to a table that was created before RLS was enforced on the schema

Philosophy of the skill

Row-level security on Postgres is the difference between "we checked org_id in the WHERE clause" and "the database rejects cross-org reads at the storage layer." Application-level checks are deleted by a single missing WHERE clause; RLS cannot be bypassed unless you use the service role explicitly. The cost is a session variable that must be set before every query and a discipline of never using the service role for application queries. Both costs are cheap relative to the consequence of a cross-tenant data leak.

Schema Pattern

-- 1. Enable and force RLS on every tenant-bound table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

-- 2. SELECT policy — only rows where org_id matches the session variable
CREATE POLICY orders_org_select ON orders
  FOR SELECT
  USING (org_id = current_setting('app.current_org_id', true)::uuid);

-- 3. INSERT policy — only allow inserts that match the session variable
CREATE POLICY orders_org_insert ON orders
  FOR INSERT
  WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);

-- 4. UPDATE policy — USING (read filter) AND WITH CHECK (write filter)
CREATE POLICY orders_org_update ON orders
  FOR UPDATE
  USING (org_id = current_setting('app.current_org_id', true)::uuid)
  WITH CHECK (org_id = current_setting('app.current_org_id', true)::uuid);

-- 5. DELETE policy
CREATE POLICY orders_org_delete ON orders
  FOR DELETE
  USING (org_id = current_setting('app.current_org_id', true)::uuid);

Application Wrapper Pattern

// lib/db.ts
import postgres from "postgres";

const sql = postgres(process.env.DATABASE_URL!);

/** Tenant-scoped query: sets app.current_org_id for every statement. */
export async function orgQuery<T>(
  orgId: string,
  fn: (sql: postgres.Sql) => Promise<T>
): Promise<T> {
  return sql.begin(async (tx) => {
    await tx`SELECT set_config('app.current_org_id', ${orgId}, true)`;
    return fn(tx);
  });
}

/** System query: bypasses RLS. Use ONLY for cron jobs, migrations, and admin. */
export async function systemQuery<T>(fn: (sql: postgres.Sql) => Promise<T>): Promise<T> {
  return fn(sql);
}

Usage in a Server Action:

import { orgQuery } from "@/lib/db";

export async function getOrders(orgId: string) {
  return orgQuery(orgId, (tx) => tx`SELECT * FROM orders ORDER BY created_at DESC`);
}

Verification

  • Every tenant-bound table has ENABLE ROW LEVEL SECURITY AND FORCE ROW LEVEL SECURITY
  • Every DML operation (SELECT, INSERT, UPDATE, DELETE) has a corresponding policy on each table
  • WITH CHECK is present on INSERT and UPDATE policies (not just USING)
  • orgQuery sets the variable inside a transaction, not at session start
  • No application code calls systemQuery (grep for systemQuery in apps/ and lib/ — any hit is a finding)
  • Every new migration that adds a table includes the RLS policy triple in the same migration file

Do NOT Use When

Use insteadWhen
systemQuery wrapperThe query legitimately crosses org boundaries (billing cron, migration backfill, admin panel)
migrate-orders-to-canonical-schemaThe task is a schema migration that also needs to update RLS policies
(a database skill without multi-tenancy scope)The application is single-tenant and org_id isolation is not a requirement

What ships with it: 1 file

349 B alongside SKILL.md

Keep looking

Skills are one crate of 326,782. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.