Hasura as an Instant GraphQL API over PostgreSQL: When It Fits
AI generated
{ }
type
GraphQL · Hasura · PostgreSQL
Hasura as an Instant GraphQL API
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.

18 min read Hasura · PostgreSQL · Row-Level Security · Actions Docker · Remote Schemas · Event Triggers

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.

11. FAQ: Hasura as an Instant GraphQL API

1What is Hasura?
A GraphQL engine that automatically generates a complete GraphQL schema from PostgreSQL via introspection, with no resolver code.
2Only for prototypes or also production?
Also production, many companies use Hasura as their primary data API with real row-level security.
3How do permissions work?
Declarative filter expressions per table and role, embedded directly into the generated SQL query, so they can't be bypassed.
4Does Hasura avoid N+1?
Yes, automatically via SQL join for nested relationship queries, with no manual DataLoader pattern.
5How to integrate custom logic?
Via actions, which appear as GraphQL mutations but internally call a custom webhook endpoint over HTTP.
6What are event triggers?
Automatic HTTP requests on data changes in watched tables, for example for sending emails or background processing.
7Combinable with an existing GraphQL API?
Yes, via remote schemas and remote joins, integrating foreign APIs seamlessly into the unified Hasura schema.
8When doesn't Hasura fit?
With complex business logic beyond pure database operations, and with schemas deliberately meant to deviate from the DB structure.
9How to manage metadata?
Via hasura metadata export/apply as part of CI/CD, not just the web console, for versioned, reviewable changes.
10Does Hasura require PostgreSQL?
PostgreSQL is best supported, other databases like MySQL or BigQuery are supported with partially reduced feature coverage.