← Blog/Field Manual Series/Article 08 · Week 11

Let Your Database Generate the API and Guard the Door.

Omenabyte Intelligence·Oct 11, 2026·5 min read
Replace your hand-built API layer — PostgREST with Row-Level Security

Most CRUD backends are the same boilerplate repeated per table: a route, a controller, a serializer, an authorization check. That's thousands of lines maintaining plumbing that doesn't encode a single real business rule.

PostgREST turns your schema directly into a documented REST API. Row-Level Security moves authorization into the database itself, enforced on every query — including ones that bypass your application entirely.

The policy

rls.sql — one policy, enforced by the database
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users see only their own notes"
  ON notes FOR SELECT
  USING (auth_user_id = NULLIF(current_setting('request.jwt.claim.sub', true), '')::uuid);

PostgREST injects request.jwt.claim.sub automatically from the caller's JWT on every request — you never set this yourself in application code.

That NULLIF(..., '') is there for a reason I only found by testing this directly. current_setting(name, true) correctly returns NULL the first time a custom setting is unset in a session — but once it's been referenced even once (say, inside a transaction that later rolled back), Postgres leaves behind an empty-string placeholder for the rest of that session. From then on, current_setting returns '', not NULL, and casting an empty string to uuid throws an error. NULLIF closes that gap for both cases.

The bypass nobody warns you about

Table owners and superusers bypass Row-Level Security by default — policies simply don't apply to them. I confirmed this directly: as the table owner, a query against a two-row table with a strict policy in place still returned both rows. If you test your policy logged in as the role that created the table, it will always look unrestricted, whether or not the policy is actually correct.

force-rls.sql — make the policy hold for the owner too
ALTER TABLE notes FORCE ROW LEVEL SECURITY;

This closes the gap if the policy needs to hold even for the owner. Either way, always test as the actual application role, not your own login.

What PostgREST auto-generates from this

generated API surface
GET  /notes   ->  rows already filtered by the policy above
POST /notes   ->  insert validated against the same policy set

Add a column, the API reflects it immediately. No controller to update.

Why this beats hand-rolling an API layer

  • ▹No controller per table — the schema is the API surface.
  • ▹Security lives in one place instead of scattered across every endpoint that happens to touch a table.
  • ▹The rule holds below the application layer — even if a client somehow calls the database directly.

Where a custom API still wins

Complex multi-step workflows, third-party orchestration, or business logic that genuinely doesn't map onto table operations — payment flows, multi-system sagas, anything truly procedural. For straightforward CRUD, you're likely maintaining code that generates itself.

Field Manual Series · Every recipe tested

This is one of eight infrastructure swaps.

Just Use Postgres is a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.

Write a controller per table, or let the schema be the API. Your call.

Up next in the series

I Replaced 8 Services With Postgres. Here's Where It Actually Breaks.

See the full series →