pREST and PostgREST both turn an existing PostgreSQL database into a REST API without hand-written handlers. PostgREST is written in Haskell, has a far bigger community, and pushes authorization into Postgres roles and row-level security. pREST is written in Go, keeps permissions in its own config, and since v2 ships a multi-database registry and a read-only MCP endpoint in the same binary. I maintain pREST, so read this knowing that, and I’ll say plainly where PostgREST is the better pick.
Table of Contents
- Where I stand
- Feature matrix
- How auth and permissions differ
- Where PostgREST is stronger
- Where pREST is stronger
- What the StackShare page gets wrong
- How I’d run each in 5 minutes
- Choose PostgREST when
- Choose pREST when
- FAQ
Where I stand
I maintain pREST. That is a bias, and it is also why I can tell you what the project does and does not do without guessing. Everything I say about PostgREST comes from its documentation and its repository, linked where I use it. If I got something wrong, tell me and I will fix it.
The short version: if you want the database to be the whole security model, use PostgREST. If you want a Go binary that fronts several Postgres clusters and lets an agent read from them through MCP, use pREST.
Feature matrix
Versions as of today: pREST v2.1.0, released on July 9. PostgREST facts come from the stable docs.
| Feature | PostgREST | pREST |
|---|---|---|
| Language and runtime | Haskell; the Linux build is a static executable, needs Postgres 14 or newer | Go; single prestd binary, Docker image prest/prest |
| How auth works | Verifies a JWT you mint elsewhere, then switches to the database role named in the token for the request; unauthenticated calls use db-anon-role |
Optional /auth endpoint that checks a users table and issues a JWT; or verify tokens from your own issuer via jwt.key, JWKS or an OpenID well-known URL |
| Permission granularity | Postgres GRANTs and row-level security policies, evaluated by the database per role |
Table and column rules in config (read, write, delete, fields), with per-user overrides matched on the JWT sub claim |
| Auto-generated routes | Tables, views and functions in the exposed schemas, e.g. /films |
Every table at /{database}/{schema}/{table} with GET, POST, PUT/PATCH, DELETE |
| Custom SQL endpoints | Database functions exposed as endpoints | _QUERIES: templated .sql files on disk, one per HTTP verb |
| Multi-database | A single db-uri per instance |
Registry of aliases; first URL segment picks the cluster, /_ready checks them all |
| MCP for AI agents | Not part of the server | Read-only /_mcp on the same port, same auth, 100 rows per call |
| Embedding and joins | Resource embedding across many-to-one, one-to-many, many-to-many, one-to-one and computed relationships | One _join per request via the query string; deeper joins go in a _QUERIES script |
| OpenAPI | Served on the root path, generated from the schema | Not in the docs; there is no generated spec |
| Row-level security | First-class; policies see current_user and the JWT claims |
Only at the level of the single role pREST connects as; no per-request role switch |
| Maturity and community | About 27.7k stars and 1.2k forks on GitHub; sponsors include Supabase, Cybertec and Neon | Nearly 4k stars and about 320 forks on GitHub; a much smaller contributor base |
| Deployment | Binary or postgrest/postgrest image, configured by file or PGRST_* env vars |
Binary, Homebrew, go install, or prest/prest image, configured by prest.toml or PREST_* env vars |
How auth and permissions differ
This is the real fork in the road, so it deserves more than a table row.
PostgREST’s docs put it in one sentence: “It is PostgREST’s job to authenticate requests – i.e. verify that a client is who they say they are – and then let the database authorize client actions.” (auth reference). The server does not issue tokens. You create a JWT from inside the database or from an external service, PostgREST checks the signature, reads the role claim, and runs SET ROLE to that database role for the duration of the request. Anything the role cannot SELECT, the API cannot return. If no token is present it switches to the anonymous role, and if db-anon-role is unset, anonymous access is blocked.
Row-level security then does the per-user filtering. The authorization guide shows a chat table where the policy is USING ((message_to = current_user) OR (message_from = current_user)), and for shared roles you read claims with current_setting('request.jwt.claims', true)::json->>'email'. Your authorization lives in SQL and applies to every client, including psql.
pREST does it the other way around. It connects to Postgres as one configured user and enforces access in its own middleware. When auth.enabled = true you get a /auth endpoint that checks a username and password against a table (prest_users by default, bcrypt in v2) and returns a JWT. You can also skip that and point jwt.jwks or jwt.wellknownurl at your identity provider, and the config reference lists HS, RS, PS, ES and EdDSA algorithms. Permissions are TOML:
[access]
restrict = true
[[access.tables]]
name = "users"
schema = "public"
permissions = ["read"]
fields = ["id", "name"]
Per-user rules go under [[access.users]] and match on the JWT sub claim. That gives you table and column granularity. It does not give you row granularity, and I am not going to pretend otherwise. If you need “users only see their own rows”, PostgREST with RLS is the cleaner design; with pREST you write a _QUERIES script that filters on a value you trust.
Where PostgREST is stronger
Maturity and ecosystem. PostgREST has roughly seven times the stars and a sponsor list that includes Supabase, Cybertec, Neon, Euronodes and Bytebase (repo). Supabase’s own docs say the platform “provides a RESTful API using PostgREST, a thin API layer on top of Postgres” (Supabase API guide), so a very large number of production apps exercise it every day. When something breaks, someone has probably hit it already.
The RLS-first model. If your team already thinks in Postgres roles and policies, PostgREST adds almost nothing new to learn and nothing new to secure.
Resource embedding. GET /films?select=title,directors(id,last_name) returns films with their director nested, and the same syntax walks one-to-many, many-to-many and computed relationships (docs). pREST’s _join handles one join per request, _join=inner:users:friends.userid:$eq:users.id, and the docs send you to a SQL script for anything deeper. For a front end that wants nested JSON, PostgREST is simply better at this.
OpenAPI. PostgREST serves a spec on / that lists every table, view and function, and you can override it with db-root-spec (docs). pREST has nothing equivalent today.
Where pREST is stronger
Multi-database routing. One pREST process can front several Postgres clusters. Register aliases in TOML or as DATABASE_ALIAS_1 / DATABASE_URL_1 pairs, and the first URL segment picks the target: GET /tenant-a/public/users. Permissions accept database and schema fields so each tenant gets its own rules, and /_ready pings every alias for Kubernetes probes (docs). I wrote up the design in the v2.0.0 and v2.1.0 release post. PostgREST takes a single db-uri, so that is one instance per database.
MCP in the same binary. v2.1.0 added /_mcp, a read-only Model Context Protocol endpoint on the same port as the REST API. It speaks JSON-RPC (initialize, tools/list, tools/call), exposes catalog tools plus a schema-aware select tool per table, caps results at 100 rows, and when auth is on it “requires the same credentials as other protected routes” (docs). An agent in Cursor or Claude Desktop gets exactly the tables your ACL allows and nothing else. The MCP server tutorial walks through the curl calls and client config.
Custom SQL as files. Drop queries/articles/search.read.sql under queries.location and it is live at GET /_QUERIES/articles/search?slug=...; the .read, .write, .update and .delete suffixes map to HTTP verbs (docs). The scripts run through the same auth stack as every other route. PostgREST wants this logic in database functions, which is a fine answer, just a different one. Some teams would rather version SQL files in the repo than migrate functions.
Go. If your backend is Go, you can import pREST as a module, write middleware or endpoint plugins, and read the code when something is unclear. That is what drew me to the project.
What the StackShare page gets wrong
If you search this comparison you will land on StackShare’s PostgREST vs pREST page. Two of its claims contradict the documentation.
It says pREST “does not have built-in support for authentication and authorization.” It does. auth.enabled turns on the /auth endpoint that issues JWTs, [jwt] verifies tokens with HMAC, RSA, ECDSA or EdDSA keys, JWKS or a well-known URL, and [access.tables] plus [access.users] give table, column and per-user rules (config reference, permissions). What is true, and what I’d rather people say, is that auth is off by default and the permission model is not row-level.
It says pREST “requires manual definition of the API endpoints in a TOML file.” It does not. Tables are exposed automatically at /{database}/{schema}/{table}; the key features page phrases it as “tables become endpoints without hand-written controllers.” The TOML file holds connection, auth and ACL settings, every one of them also available as a PREST_* environment variable, and the config page notes that since v1.2.0 prestd runs with no config file at all.
The page also quotes the project’s original motivation, that keeping Haskell software in production “is not easy job.” That was the project’s early pitch, and I would put it more gently today: PostgREST ships a static binary, so you rarely touch Haskell to run it. My honest version is that a Go team will find pREST easier to patch and extend.
Where StackShare says PostgREST is more mature and probably better optimized for high concurrency, I have no benchmark that says otherwise, so I will not argue with it.
How I’d run each in 5 minutes
Both assume a Postgres you can reach. Commands come from each project’s docs.
PostgREST, following the install page and the tutorial. The database side needs an anonymous role and a login role first:
create role web_anon nologin;
grant usage on schema api to web_anon;
grant select on api.todos to web_anon;
create role authenticator noinherit login password 'mysecretpassword';
grant web_anon to authenticator;
docker run --rm --net=host \
-e PGRST_DB_URI="postgres://authenticator:mysecretpassword@localhost:5432/postgres" \
-e PGRST_DB_SCHEMAS="api" \
-e PGRST_DB_ANON_ROLE="web_anon" \
postgrest/postgrest
curl http://localhost:3000/todos
On a Mac, swap --net=host for -p 3000:3000 and point the URI at a reachable host. The env var names follow the PGRST_ plus upper-snake-case rule from the configuration reference.
pREST, following the Docker deployment page. No role setup; it uses whatever user is in the URL:
docker run -d -p 3000:3000 \
-e PREST_PG_URL=postgres://username:password@hostname:5432/dbname \
-e PREST_DEBUG=true \
prest/prest:v2.1.0
curl http://localhost:3000/dbname/public/todos
curl -s http://localhost:3000/_mcp | head
PREST_DEBUG=true skips JWT enforcement for local work; drop it and set PREST_JWT_KEY before anything faces the internet. For a stack with auth already wired, the start with Docker page has a compose file, a prestd migrate up auth step and the curl -X POST /auth call that returns your first token.
Choose PostgREST when
- You already model authorization as Postgres roles and RLS policies, or you want to start doing that.
- You are on Supabase. You are running PostgREST whether you noticed or not.
- Your front end wants nested JSON from one request. Resource embedding is better than anything pREST offers.
- You want an OpenAPI spec generated from the schema without extra tooling.
- You want the larger community, and you do not need the first two things below.
Choose pREST when
- One API has to front several Postgres clusters or tenants, with per-alias permissions and a single readiness probe.
- You want AI agents reading from Postgres through MCP with the same ACL as your REST clients, and you do not want a separate MCP process.
- Your team writes Go and will want to extend the server with plugins or read its source.
- You prefer custom SQL as versioned
.sqlfiles hit over HTTP rather than database functions. - Table and column permissions are enough, and your tenancy boundary is a database or a schema rather than a row.
If you are still unsure, run both against a copy of your schema for an afternoon; each starts in one command. I take questions on the pREST side, and my contact details are on the bio page.
FAQ
Is pREST a PostgREST alternative?
Yes. Both expose an existing PostgreSQL database as a REST API without writing handlers. PostgREST is a Haskell server that delegates authorization to Postgres roles and row-level security, while pREST is a Go server with config-based table and column permissions, multi-database routing and a built-in read-only MCP endpoint.
Can I migrate from PostgREST to pREST?
Not automatically. The URL shapes differ (/films?select=... versus /{database}/{schema}/films?_select=...), the auth model moves from database roles to pREST’s JWT and access.tables rules, and RLS policies that depend on current_user stop doing per-user work because pREST connects as one role. Plan it as a client rewrite plus a permissions redesign, and only do it for the multi-database or MCP features.
Does pREST support row-level security?
Not per request. pREST connects as a single configured Postgres user, so any RLS policy applies to that user for every call; there is no SET ROLE to the end user the way PostgREST does it. Per-user restrictions in pREST are table and column level through [[access.users]], and per-row filtering belongs in a _QUERIES script.
Which is faster, pREST or PostgREST?
I do not have a benchmark I trust enough to publish, so I will not claim either way. Both are thin layers and the database usually dominates the latency. StackShare asserts PostgREST is better optimized for high concurrency; measure on your own workload before you believe either of us.
Which should a Supabase user pick?
PostgREST. Supabase’s REST API is PostgREST, so you already have it, with RLS, embedding and the client libraries built around it. pREST only makes sense for a Supabase user who needs to front additional Postgres clusters or expose a read-only MCP endpoint outside the Supabase stack.