Why a generated API rather than a hand-written backend
In most applications, a large part of the server code merely translates HTTP requests into SQL: list, filter, paginate, create, update, delete, checking each time that the user is allowed to. That code is repetitive, it needs tests, and every new column means touching it again.
Hasura starts from the database schema. A tracked table becomes a GraphQL type, a foreign key becomes a nested field, and a permission rule written once applies to every query. Your own business logic — a payment, an email, a calculation — stays in your code, which Hasura calls by webhook. You keep the PostgreSQL database: nothing is locked into a proprietary format.
What Hasura brings
- Instant GraphQL API — queries, mutations, filters, sorting, pagination and aggregates on every tracked table.
- Relationships — foreign keys become nested fields, queryable in a single request.
- Real-time subscriptions — a client subscribes to a query and receives changes over WebSocket.
- Role-based permissions — row- and column-level rules, driven by the claims of a JWT.
- Actions and triggers — webhooks called by a mutation, by a data change or on a cron schedule.
- Console and REST endpoints — web administration, GraphiQL explorer, and turning a query into a REST route.
Hasura versus other ways to expose PostgreSQL
Scroll the table
| Hasura | Hand-written API | PostgREST | |
|---|---|---|---|
| API generated from the schema | Yes, GraphQL and REST | No | Yes, REST |
| Nested relationships in one request | Yes | Depends on the code | Yes, via embedding |
| Real time | GraphQL subscriptions | To be built | No |
| Permissions | Per role, row and column, in the console | In the code | PostgreSQL roles and RLS |
| Business logic | Actions and triggers as webhooks | Free | SQL functions |
| Admin interface | Web console | No | No |
Prerequisites
An Ubuntu 24.04 VPS with 1 GB of RAM is enough to start: at rest, the Hasura engine and PostgreSQL use about 200 MB together. Plan for 2 GB as soon as the database grows or subscriptions multiply. You need Docker with the Compose plugin, and a domain name pointing to the VPS, with nginx and an HTTPS certificate: your API will be called by browsers and mobile apps, so it must answer on a stable, encrypted address.
Install Hasura with Docker Compose
Prepare the secrets
Create the application directory and a
.envfile holding the database password and the admin secret, the API's master key. The file is readable by root only.mkdir -p /opt/hasura && cd /opt/hasura cat > .env <<EOF DB_PASSWORD=$(openssl rand -hex 16) ADMIN_SECRET=$(openssl rand -hex 24) EOF chmod 600 .envCreate the Compose file
This
docker-compose.ymlstarts PostgreSQL on the stack's internal network, with no published port, then Hasura once the database is ready. Hasura's port is published on the loopback only; the console is served from the image, with no call to the vendor's CDN, and telemetry is off.services: postgres: image: postgres:17.11-alpine restart: unless-stopped environment: POSTGRES_DB: hasura POSTGRES_USER: hasura POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - hasura-pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U hasura -d hasura"] interval: 10s retries: 10 hasura: image: hasura/graphql-engine:v2.51.3 restart: unless-stopped depends_on: postgres: condition: service_healthy ports: - "127.0.0.1:8103:8080" env_file: .env environment: HASURA_GRAPHQL_DATABASE_URL: postgres://hasura:${DB_PASSWORD}@postgres:5432/hasura HASURA_GRAPHQL_ADMIN_SECRET: ${ADMIN_SECRET} HASURA_GRAPHQL_ENABLE_CONSOLE: "true" HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets HASURA_GRAPHQL_DEV_MODE: "false" HASURA_GRAPHQL_ENABLE_TELEMETRY: "false" volumes: hasura-pgdata: {}Start the stack
On first start, Hasura creates its metadata tables in the
hdb_catalogschema and connects the database as thedefaultsource. The/healthzendpoint answersOKwhen the engine is ready.docker compose up -d curl -fsS http://127.0.0.1:8103/healthz # OKPublish over HTTPS behind nginx
Add a
serverblock for your domain, with a Let's Encrypt certificate. TheUpgradeandConnectionheaders are required: subscriptions go through WebSocket.location / { proxy_pass http://127.0.0.1:8103; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; }
The admin secret never leaves the server
Without HASURA_GRAPHQL_ADMIN_SECRET, the console and the whole API are open to anyone who knows the address. With it, it grants full rights: read and write every table, run SQL, change the metadata. Keep it for the console and your admin scripts, and never put it in frontend or mobile app code.
Create a table and query it
Open https://api.example.com/console, enter the admin secret, then, in the Data tab, create a todos table with the columns id (auto-increment integer), title (text) and done (boolean). Click Track: the table immediately shows up in the explorer of the API tab. From an admin script, the same query is sent with the admin secret:
curl -s https://api.example.com/v1/graphql \
-H "x-hasura-admin-secret: $ADMIN_SECRET" \
-H 'content-type: application/json' \
-d '{"query":"{ todos { id title done } }"}'Authenticate your users and set permissions
Your users must not use the admin secret: they present a JWT issued by your identity provider (Logto, Authentik, Zitadel, Auth0…). Tell Hasura where to find the verification keys, and which role to give requests without a token, in the .env file:
HASURA_GRAPHQL_JWT_SECRET={"type":"RS256","jwk_url":"https://auth.example.com/oidc/jwks"}
HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymousRestart the stack with docker compose up -d: the hasura service reads this file through env_file. The token must carry the x-hasura-default-role, x-hasura-allowed-roles and x-hasura-user-id claims. In each table's Permissions tab, then define what each role can read, insert or update — for example, the user role only sees rows where user_id equals X-Hasura-User-Id.
Troubleshooting
The console shows "invalid x-hasura-admin-secret": the secret you entered does not match the value in .env. Check it with grep ADMIN_SECRET /opt/hasura/.env, then reload the page.
Hasura keeps restarting: the database is unreachable. docker compose logs hasura shows the connection error; check that the password in the HASURA_GRAPHQL_DATABASE_URL URL is PostgreSQL's. If you changed it after the first start, PostgreSQL kept the old one: it only reads POSTGRES_PASSWORD when the volume is created.
Subscriptions drop: the reverse proxy does not pass the WebSocket upgrade. Add the Upgrade and Connection headers shown above, and raise proxy_read_timeout if your clients stay idle for long.
A table does not appear in the API: it exists in the database but is not tracked. Click Track in the Data tab, or track every table of the schema at once.
One-click install from the ServOrbit Marketplace
The ServOrbit Marketplace installs Hasura on your VPS without a command line: it deploys PostgreSQL and the Hasura engine, generates the admin secret and the database password, connects the database as the default source and publishes the API over HTTPS on a subdomain of your domain or on the VPS's free subdomain. The admin secret is shown in your client area; all that is left is to open the console and create your tables.