This article extends two comparisons already published on this blog: our review of PostgREST compatibility and its alternatives and our comparison dedicated to GraphQL layers on Postgres. Here the angle changes: a decision grid between three ways to build an API layer, with Hasura treated for its permissions and business extension points rather than its GraphQL syntax, and a hand-written API as an option in its own right, not a simple "else" line at the bottom of the table.
The essentials
- PostgREST auto-generates a REST API from the Postgres schema — no arbitrary business logic possible, the RLS remains the only security boundary.
- Hasura adds its own permissions system by role and table, Actions to connect a business webhook, Event Triggers, and RESTified endpoints on top of its GraphQL engine.
- A custom API (Node.js, Express, Fastify...) gives total control over business logic, validation and auth — at the cost of writing, testing and maintaining everything yourself.
- On Aurabase, the PostgREST layer is a real instance; business logic beyond CRUD goes through SQL functions exposed in RPC or through Edge Functions, not through a separate Node server to host.
- The three approaches are not necessarily mutually exclusive: combining PostgREST for CRUD and a custom API for sensitive operations is a common pattern in production.
The real choice: who writes the business logic, and where
The question “PostgREST or Hasura or custom API” hides a more useful question: who writes your business logic, with which tool, and who uses this code in production? The three architectures respond differently, and this difference structures everything else — security, speed of implementation, technical debt in the long term.
| Origin of the API | Generated from SQL schema | Generated from the schema, via the GraphQL Hasura engine | Written route by route, by hand |
|---|---|---|---|
| Custom business logic | SQL functions (RPC) only | Actions (webhook) + Event Triggers | Any code, without tool constraints |
| Security Model | RLS Postgres, role driven by JWT | Permissions specific to role/table, not a delegation to the RLS | What you code (middleware, ORM, optional RLS) |
| To accommodate in addition | Nothing — a lightweight binary | The Hasura engine, with its own metadata base | The complete application server |
| Learning curve | Low if the team already knows how to write SQL | Medium — a new permissions and config system | Nothing on the tool, but everything else to design |
| POSTGREST | HASURA | CUSTOM API |
None of the three columns is strictly better: each shifts the work elsewhere. PostgREST moves it to SQL, Hasura to configuration and webhooks, a custom API to classic application code.
PostgREST: the API as a direct reflection of the schema
PostgREST transforms your Postgres schema into a REST API — filters, relationship embedding, RPC, RLS driven by JWT — with no backend to write. We detail this scope in depth in our article on its real compatibility and its alternatives; what matters for this comparison is where PostgREST stops.
PostgREST has no notion of arbitrary business logic. Each rule must be expressed in SQL: an RPC function, a trigger, a constraint, an RLS policy. This is an accepted constraint, not an oversight — the diagram remains the only source of truth, which eliminates any drift between an application layer and the base it serves.
Concretely, it is impossible to call a third-party payment service, send a confirmation email, or calculate a score in JavaScript from a direct PostgREST request. This logic must either live in SQL (pl/pgsql function), or be triggered outside - a trigger which publishes an NOTIFYevent, listened to by an external service which is no longer PostgREST.
Hasura: permissions declared, business logic grafted by webhook
Our article on GraphQL layers on Postgres details where the Hasura engine runs and how its permissions differ from the Postgres RLS. Here, the angle is that of business logic: how to plug custom code into a database managed by Hasura, and where.
A Action Hasura exposes a custom GraphQL mutation or query, backed by an HTTP webhook that you write in the language of your choice. Hasura validates inputs according to the declared schema, calls your webhook, then returns its response to the client. It is the gateway to any logic that goes beyond CRUD: call to a payment provider, complex calculation, multi-step orchestration.
The Event Triggers follow the opposite direction: an insert, an update or a delete on a table triggers a webhook, asynchronously and with automatic restart in the event of failure. This is the mechanism that most Hasura integrations use to synchronize a third-party service (billing, transactional email, search engine) without coupling this code to the initial customer request.
Hasura can also expose an already-written GraphQL query as a typical REST route, with a named path and parameters — its RESTifiedendpoints, in the terminology of its own documentation. Useful if your frontend team prefers to consume REST, without giving up the underlying GraphQL permissions engine.
Hasura permissions are a Hasura-specific system, per role and per table, not a delegation to the Postgres RLS. Two places to audit access rules rather than just one — a real cost to weigh against the flexibility gained from Actions and Event Triggers.
Context reminder, developed in our dedicated article: Hasura has refocused its communication on PromptQL, a layer designed for AI agents, since June 2025 — without removing its GraphQL engine, still presented as “battle-tested” on its official website.
Custom API (Node.js, Express, Fastify): code everything, control everything
A hand-written API has, by definition, no limits: any business logic, in any language, with any dependencies. It's also the only one of the three options where nothing is generated for you — every route, every validation, every connection to the database is code that you own and must maintain.
What this model offers, in exchange for manual work: full control over errors and returned HTTP codes, classic testability (handlers, not declarative configuration), and no new DSL to learn for a team that already masters its language.
What it costs, in exchange: CRUD, pagination and filters to write and maintain by hand for each resource; authentication and authorization to implement and audit yourself, without automatically inherited RLS; the risk of N+1 queries if each nested relationship triggers its own Postgres query without discipline; and API documentation to be maintained manually, or via a third-party generator to integrate.
On raw performance, the question “Is Node.js slower than Rust” is a topic in its own right — our article on Rust vs Node.js latency covers it in detail, with methodology declared upstream on our benchmark methodology page. A custom API has the same performance profile as any HTTP service you already use, neither better nor worse by construction. To know precisely where PostgREST saturates and at what point a custom layer becomes necessary, see our article on the real limits of PostgREST in production.
Comparison table: the three options side by side
Beyond the architecture, four criteria most often come up when choosing: speed of implementation, real business flexibility, long-term technical debt, and the typical use case where each option is most comfortable.
| Initial setup | Minutes — schema already exists | Hours — connect the base, configure permissions | Days to weeks — write each route |
|---|---|---|---|
| Business flexibility | Limited to SQL (RPC, triggers) | Good via Actions/Event Triggers, but goes through an external webhook | Total, straightforward |
| Term technical debt | Weak — the diagram remains the only source of truth | Medium — Hasura metadata to maintain in addition to the schema | High if the team grows without discipline (tests, documentation, review) |
| Typical use case | Direct CRUD on a stable schema, team comfortable in SQL | Federate several data sources, or AI agent-oriented logic | Complex business logic, numerous third-party integrations |
| POSTGREST | HASURA | CUSTOM API |
Where does business logic go on an Aurabase project
On an Aurabase Postgres engine project, the CRUD layer is already covered by an actual PostgREST instance, not an approximate reimplementation. The question that remains open for this comparison: where to write what goes beyond the CRUD?
Two paths exist, and they are not mutually exclusive. The first: an SQL function exposed in RPC, for any logic that remains reasonable to express in SQL — calculation of a total, cross-validation between several tables, cascading updates in a single transaction.
The second way: Edge Functions, for everything that goes beyond the SQL domain — calling a payment API, sending an email, calculating an embedding. Two paths lead there at Aurabase: the Studio editor, which runs Deno (TypeScript) code exactly as on Supabase, and the aura functions deployCLI, which aims for a separate path for functions written in Rust and compiled in WASM — detailed in our unified Rust architecture. Neither path requires hosting a separate Node server, unlike the pure “custom API” option in this article, where that server is entirely your responsibility.
This distribution is not a shaky compromise between the three models compared here: it is literally PostgREST for CRUD, a brick close to Hasura Actions for event logic via RPC and triggers, and Edge Functions which avoid the operation of a complete application server - without ever forcing a binary choice between "all PostgREST" and "all custom".
How to choose according to your context
Four situations come up most often. The right choice depends mostly on what your business logic requires, not on the popularity of a tool.
- Your schema is stable and your business logic is in SQL. Self-hosted PostgREST, or natively integrated (Aurabase, Supabase), is enough: nothing more to host, and the schema remains the only source of truth.
- You want to bring together several data sources, or your roadmap is geared towards AI agents consuming your data. Hasura, with its PromptQL layer, fits this terrain better.
- Your product has rich business logic, numerous third-party integrations, and a team already equipped with an application language. A custom API remains the most direct choice, at the cost of writing it and maintaining it over time.
- You want self-generated CRUD without giving up real space for business logic (RPC, Edge Functions), without stacking one more application service to exploit. This is the angle documented in this comparison applied to Aurabase, previous section.