The Problem That Pushed Me Toward Hasura
Six months ago I was juggling three separate projects at once — one on MySQL, one on PostgreSQL, one on MongoDB. Each database has its genuine strengths. But every time the schema changed, I had to update resolver functions, REST controllers, and auth middleware in lockstep. One afternoon I spent four hours tracking down why a single renamed column broke three API endpoints and a mobile app. That debugging session finally convinced me something had to change.
The question wasn’t “which database is best” — it was “why am I hand-writing CRUD wrappers for the hundredth time?”
Root Cause: The API Layer Tax
Traditional backend development carries what I call the API layer tax. You have a perfectly good relational schema in PostgreSQL, but to expose it you need to:
- Write route handlers or resolvers for every table and relationship
- Implement filtering, pagination, and sorting logic separately
- Add authentication middleware and wire it to row-level checks
- Build WebSocket infrastructure if you need realtime updates
- Keep all of this synchronized with every schema migration
For a 20-table schema, that easily balloons into thousands of lines of boilerplate before you’ve written a single line of actual business logic. Most of it is purely mechanical work — it follows the schema deterministically. Yet humans write it by hand, and that’s where the bugs creep in.
Solutions I Evaluated
Option 1: Manual REST API (FastAPI / Express)
Full control, familiar tooling, easy to customize. But the maintenance cost is real. Every ALTER TABLE becomes a multi-file change. I kept this for services with complex business rules, but not for data-heavy CRUD.
Option 2: PostgREST
PostgREST auto-generates a REST API directly from a PostgreSQL schema. Lightweight and fast. But REST means the client has no say over field selection — you can’t ask for exactly the fields you need, and there’s no built-in realtime. For read-heavy dashboards with GraphQL clients, it didn’t fit.
Option 3: Prisma + Apollo GraphQL Server
You get type-safe queries and a proper GraphQL layer with this combination. But you still write resolvers, manage the Prisma client, and configure subscriptions separately. An improvement over raw REST — not an elimination of the boilerplate.
Option 4: Hasura GraphQL Engine
Hasura connects to your existing PostgreSQL database and instantly generates a complete GraphQL API — queries, mutations, subscriptions — with no code. Permissions are configured through a UI or metadata YAML, not scattered across middleware. After six months in production, it’s the one I’d recommend for teams building data-driven apps without a large backend team.
Setting Up Hasura with Docker
The fastest way to run Hasura locally is with Docker Compose. Create a docker-compose.yml:
version: '3.6'
services:
postgres:
image: postgres:16
restart: always
environment:
POSTGRES_PASSWORD: mysecretpassword
volumes:
- db_data:/var/lib/postgresql/data
graphql-engine:
image: hasura/graphql-engine:v2.40.0
ports:
- "8080:8080"
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:mysecretpassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"your-256-bit-secret-here"}'
depends_on:
- postgres
volumes:
db_data:
Start it:
docker compose up -d
Open http://localhost:8080/console and enter your admin secret. Setup done. Hasura is now pointing at your PostgreSQL instance.
Tracking Tables and Relationships
Hasura doesn’t expose tables automatically — you “track” them through the console or API. This is intentional: you might have internal tables (audit logs, migration history) you don’t want in your public API.
Say you have these tables:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
title TEXT NOT NULL,
body TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
In the Hasura console, go to Data → public → Track All. Hasura detects foreign keys and suggests relationships. Accept the user → posts array relationship and the post → user object relationship. Now you can query nested data in a single request:
query GetUserPosts {
users {
email
posts(where: { published: { _eq: true } }) {
title
created_at
}
}
}
No resolver code. The relationship traversal, join, and filtering all happen automatically.
Row-Level Security Without Custom Middleware
The permissions model caught me off guard — in a good way. In traditional setups, you either use PostgreSQL RLS policies directly (complex to manage at scale) or write authorization checks in every resolver (error-prone and easy to miss). Hasura takes a third path: define permission rules per role in the console, and it translates them into WHERE clauses before touching the database.
Example: users should only read their own posts.
In the console, go to Data → posts → Permissions → user role → Select, then set the row permission:
{
"user_id": { "_eq": "X-Hasura-User-Id" }
}
X-Hasura-User-Id is a claim extracted from the JWT token Hasura receives on every request. When a logged-in user queries posts, Hasura automatically appends WHERE user_id = '<their-id>'. They cannot see anyone else’s posts — no matter what they put in the query.
Column-level visibility, row count limits, and separate rules for insert, update, and delete are all configurable per role from the same interface.
Realtime Subscriptions Without the WebSocket Plumbing
Implementing WebSocket-based realtime in a custom backend takes a weekend minimum. Connection handling, heartbeats, reconnect logic — it adds up fast. In Hasura, you change query to subscription:
subscription WatchNewPosts {
posts(
where: { published: { _eq: true } },
order_by: { created_at: desc },
limit: 10
) {
id
title
created_at
}
}
The client receives an update whenever the result set changes. Hasura polls PostgreSQL on a configurable interval (default 1 second) and pushes diffs to connected clients over WebSocket. For a live notification feed or dashboard, you get working realtime with no extra infrastructure to manage.
Managing Hasura in Production
Six months of production use taught me three practices that matter more than they look:
Use Hasura Metadata for Version Control
All permissions, relationships, and tracked tables are stored as metadata YAML files. Export them and commit to git:
# Install Hasura CLI
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
# Initialize project
hasura init my-project --endpoint http://localhost:8080 --admin-secret myadminsecretkey
cd my-project
# Export current metadata
hasura metadata export
# Apply metadata to another environment
hasura metadata apply --endpoint https://staging.example.com
Promoting environments — local to staging to production — becomes a single command.
Never Expose the Admin Secret to Clients
The admin secret bypasses all permission rules. Set HASURA_GRAPHQL_ADMIN_SECRET only in server environments and use JWT tokens for all client-facing requests. Your auth service (Auth0, Supabase Auth, or a custom JWT issuer) issues tokens with Hasura-specific claims:
{
"sub": "user-uuid-here",
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["user"],
"x-hasura-default-role": "user",
"x-hasura-user-id": "user-uuid-here"
}
}
Use Actions for Business Logic
Hasura doesn’t replace backend code entirely — it replaces the data layer. For operations with side effects (sending emails, charging payments, calling third-party APIs), use Hasura Actions: define a custom GraphQL mutation that Hasura forwards to an HTTP handler you write. Data mutations stay in Hasura’s type-safe layer; side effects live in your handler.
Where Hasura Fits — and Where It Doesn’t
After six months, here’s my honest take: Hasura saves enormous time for data-heavy applications where the schema is the product. Internal tools, dashboards, admin panels, and mobile backends with standard CRUD patterns are its sweet spot.
Heavy transformation logic between the database and the client is a different story. Teams deeply invested in REST — and not ready to shift frontend querying to GraphQL — will find the migration cost outweighs the gains.
But if your team spends weekends writing “just another list endpoint with filters and pagination,” give Hasura a proper day. My PostgreSQL tables were tracked, permissioned, and serving a React frontend in under two hours — time I spent on actual product features instead.

