over PostgreSQL: when the approach pays off
Hasura turns an existing PostgreSQL database into a complete GraphQL API within seconds, including filtering, sorting, pagination and relationships, without writing a single resolver. The approach fits certain scenarios extremely well and is completely unsuitable for others. This article shows exactly where that line runs.
Table of contents
- 1. What Hasura is and how it generates GraphQL from PostgreSQL
- 2. Setup: Docker, metadata and the first table
- 3. Permissions: row-level security in the permission system
- 4. Relationships: modeling object and array relationships
- 5. Custom logic: actions and event triggers
- 6. Remote schemas and remote joins
- 7. Performance: query caching and connection pooling
- 8. When Hasura does NOT fit
- 9. Hasura vs. hand-written GraphQL
- 10. Summary
- 11. FAQ
1. What Hasura is and how it generates GraphQL from PostgreSQL
Hasura is a GraphQL engine that connects to an existing PostgreSQL database, reads the database schema via introspection, and automatically generates a complete GraphQL schema with queries, mutations and subscriptions for every table. Unlike graphql-php, Lighthouse or NestJS GraphQL, nobody writes resolver code with Hasura, query behavior gets translated directly into performant SQL, with optimized joins for nested relationship queries.
The central misconception on first contact with Hasura is treating it as a pure prototyping tool. In fact, numerous companies run Hasura in production as their primary data API, because the generated queries are typically more efficient than naively written resolver chains, and because the built-in permission engine enforces real row-level security at the database level, not just at the application level. For domains whose GraphQL structure hangs closely off the relational schema, Hasura is genuinely production-ready, not merely a development aid.
2. Setup: Docker, metadata and the first table
The fastest way into Hasura is via Docker Compose, which starts the Hasura GraphQL Engine container alongside a PostgreSQL instance. After startup, Hasura connects to the database through a connection string configuration and shows every existing table in the built-in console interface, each one activatable as a GraphQL type with a single click. This configuration, which tables, views and functions get exposed, ends up in versionable YAML metadata files that you can manage in Git like ordinary code.
Important for production setups: metadata files should never be modified exclusively through the web console, but rather via hasura metadata export and hasura metadata apply as part of a CI/CD workflow, so changes to permissions, relationships and actions can be reviewed traceably, instead of accumulating unversioned in the running instance.
# docker-compose.yaml — Hasura GraphQL Engine with PostgreSQL
version: "3.6"
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgrespassword
volumes:
- db_data:/var/lib/postgresql/data
graphql-engine:
image: hasura/graphql-engine:v2.40.0
ports:
- "8080:8080"
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_ADMIN_SECRET: changeme
depends_on:
- postgres
volumes:
db_data:
-- products table — Hasura tracks this and exposes it as a GraphQL type instantly
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sku TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL,
category_id UUID REFERENCES categories(id),
created_at TIMESTAMPTZ DEFAULT now()
);
3. Permissions: row-level security in the permission system
The Hasura permission system defines separate rules per table and role for select, insert, update and delete, each as a declarative filter expression similar to a SQL WHERE clause. A rule like {"customer_id": {"_eq": "X-Hasura-User-Id"}} ensures an authenticated customer can only see their own orders, where X-Hasura-User-Id is a session variable set by the authentication layer, usually via JWT claims.
The decisive advantage of this model over authorization logic in resolver code: the row-level permission gets embedded directly into the generated SQL query as an additional WHERE condition, so there's no way to accidentally bypass it, because it doesn't exist as a separate, potentially forgotten check step, it's structurally part of every single query. At the column level, Hasura additionally offers column permissions, letting you make a field like internal_notes visible only to the admin role.
// Hasura metadata — row-level permission for role "customer" on table "orders"
{
"table": { "schema": "public", "name": "orders" },
"select_permissions": [
{
"role": "customer",
"permission": {
"columns": ["id", "status", "total", "created_at"],
"filter": {
"customer_id": { "_eq": "X-Hasura-User-Id" }
},
"limit": 100
}
}
]
}
4. Relationships: modeling object and array relationships
Relationships between tables are configured in Hasura as an object relationship for many-to-one, and an array relationship for one-to-many or many-to-many, each based on the existing foreign key constraints in PostgreSQL. If a foreign key already exists, Hasura detects the relationship automatically and suggests it for activation, otherwise the relationship can also be defined manually via arbitrary column combinations, for example for legacy schemas without clean foreign key constraints.
Nested GraphQL queries over these relationships get translated automatically by Hasura into efficient SQL joins, entirely avoiding the classic N+1 problem typical of hand-written resolvers without DataLoader. A query for 50 products, each with a related category, produces a single SQL query with a JOIN in Hasura, not 51 separate queries, a structural performance advantage over naively implemented resolver-based APIs.
# Nested query resolved by Hasura as a single SQL JOIN, no manual DataLoader needed
query ProductsWithCategory {
products(where: { price: { _gt: 20 } }, order_by: { created_at: desc }) {
id
sku
name
price
category {
id
name
}
}
}
5. Custom logic: actions and event triggers
For business logic that can't be expressed as a pure database query, for example processing a payment through an external payment provider, Hasura offers actions: custom GraphQL mutations that show up in the generated schema, but internally trigger an HTTP request to a webhook endpoint you write yourself. The actions mechanism lets you seamlessly plug arbitrary custom code, for example in Node.js or Go, into the GraphQL schema, without giving up the auto-generation approach for the rest of the API.
Event triggers go the opposite direction: instead of reacting to a GraphQL request, they fire automatically whenever data changes in a watched table, for example after every INSERT into the orders table. The trigger sends an HTTP request to a configured endpoint, ideal for tasks like sending confirmation emails or kicking off asynchronous background processing, without the database itself needing trigger logic in PL/pgSQL.
6. Remote schemas and remote joins
For cases where an already existing GraphQL API, for example a separate microservice, needs to be combined with the Hasura-generated data, Hasura offers remote schemas: a foreign GraphQL schema gets registered as an additional data source and shows up in the unified Hasura schema right alongside the database-generated types, as if everything came from a single API. That's excellent for gradually migrating existing systems toward Hasura, without having to rebuild everything at once.
Remote joins go a step further and let you link fields from the remote schema directly to local database types, so a product from the database can reference an inventoryStatus field from a separate inventory microservice as though it were an ordinary database column. That capability turns Hasura into a genuine API gateway alternative for teams that want to unify various data sources under a single GraphQL endpoint.
7. Performance: query caching and connection pooling
Hasura ships with built-in query response caching that caches frequently repeated queries based on their structure and variables, configurable via a @cached directive right in the GraphQL query. For read access to rarely-changing data, for example a product category list, that reduces database load significantly, without having to manually integrate a separate caching layer like Redis.
For connection pooling to the database, Hasura uses an internal pool by default, whose size is configurable via HASURA_GRAPHQL_PG_CONNECTIONS. Under high concurrency, it's also worth adding an external pooler like PgBouncer in front, because PostgreSQL itself works with a limited number of concurrent connections, and an uncontrolled, growing Hasura pool quickly hits that limit during load spikes.
8. When Hasura does NOT fit
As powerful as Hasura's auto-generation approach is, it's structurally unsuited to certain scenarios. Domains with complex, multi-step business logic, for example an order process with discount rules, inventory checks and multiple validation steps, don't translate sensibly into a pure database operation. You can solve that via actions, but then most of the actual logic moves right back into hand-written code anyway, and Hasura's auto-generation advantage shrinks accordingly.
Also unsuitable is Hasura for GraphQL schemas that are deliberately meant to deviate significantly from the relational database structure, for example when the API design should look different from the underlying table schema for domain reasons. And for teams that need full control over every aspect of query execution, for example very specific per-field custom caching, the declarative Hasura approach is more limiting than a hand-written graphql-php or NestJS solution with completely free-form resolvers.
9. Hasura vs. hand-written GraphQL
The decision between Hasura and a manually written GraphQL API depends heavily on how tightly the domain is coupled to the relational database structure.
| Criterion | Hasura | Hand-written GraphQL |
|---|---|---|
| Time to first working API | Minutes | Days to weeks |
| N+1 avoidance | Automatic via SQL join | Manual via DataLoader |
| Complex business logic | Only via actions, with custom code | Native in the resolver |
| Schema structure vs. DB structure | Tightly coupled | Freely chosen |
| Row-level security | Built in, declarative | Has to be implemented yourself |
For data-driven applications with predominantly CRUD-like access patterns and clear row-level permissions, for example internal admin tools, dashboards or backend-for-frontend layers over an existing database, Hasura is often the faster, lower-maintenance choice. For domains with deep, branching business logic or an API design that deliberately deviates from the database structure, a hand-written solution with graphql-php, Lighthouse or NestJS remains the more suitable foundation.
Mironsoft
GraphQL, PostgreSQL and API architecture for data-intensive systems
Checking whether Hasura fits your database?
We assess your PostgreSQL structure, set up Hasura with clean row-level permissions and relationships, and give you an honest answer on when actions or a hand-written GraphQL solution are the better choice.
Hasura setup
Setting up metadata-based configuration, versioned and CI/CD-ready
Permissions & security
Modeling row- and column-level permissions to fit your role model
Architecture review
An honest assessment of when Hasura fits and when it doesn't
10. Summary
Hasura generates a complete, performant GraphQL API from a PostgreSQL database, with row-level permissions, automatically detected relationships and built-in N+1 avoidance through SQL joins, without writing a single resolver. For data-driven applications with predominantly CRUD-like access patterns, that cuts development time from weeks to minutes while delivering a declarative configuration you can version in metadata.
The auto-generation approach does hit clear limits, though: complex, multi-step business logic that can't be expressed as a database operation, and schemas deliberately meant to deviate from the relational structure, both fit Hasura poorly. Actions and remote schemas soften that limitation, but don't fully replace the flexibility of a hand-written graphql-php, Lighthouse or NestJS solution. The right choice ultimately depends on how tightly your own domain is coupled to the relational database schema.
Hasura as an Instant GraphQL API — Key Takeaways
Auto-generation
Complete GraphQL schema directly from PostgreSQL introspection, versioned in metadata YAML files.
Permissions
Row- and column-level security embedded in every generated SQL query, not bypassable like resolver-based checks.
Actions & remote schemas
Custom code and external APIs integrate seamlessly into the generated schema.
Limits
Complex business logic and heavily deviating schema designs fit the approach poorly.