Let Your Database Generate the API and Guard the Door.

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
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.
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
GET /notes -> rows already filtered by the policy above
POST /notes -> insert validated against the same policy setAdd 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 →