Simple all in one redy to go backend: Hasura for API, Keycloak for Auth API, Postgres as Database.
| Service | Purpose | Default |
|---|---|---|
postgres |
Database | always on |
hasura-api |
Production GraphQL API | always on |
hasura-admin |
Console, metadata, config, pgdump | optional private service |
hasura-cli |
Migrations and metadata tool | profile tools |
keycloak |
OpenID Connect provider | always on |
| Port | Service | Access |
|---|---|---|
8080 |
GraphQL API | public |
8082 |
Keycloak | public |
8081 |
Admin Hasura | private |
9695 |
CLI Console | local only |
Do not expose 8081 to the internet.
Create .env:
POSTGRES_PASSWORD=postgres
HASURA_GRAPHQL_ADMIN_SECRET=dev-secret
KEYCLOAK_ADMIN_PASSWORD=dev-keycloakOptional env vars for dedicated Hasura metadata storage and Keycloak storage:
# Defaults to POSTGRES_DB (or app)
POSTGRES_METADATA_DB=hasura_metadata
# Defaults to POSTGRES_USER (or hasura)
POSTGRES_METADATA_USER=hasura_metadata
# Defaults to POSTGRES_PASSWORD
POSTGRES_METADATA_PASSWORD=metadata-password
# Defaults to POSTGRES_DB (or app)
KEYCLOAK_DB_DATABASE=keycloak
# Defaults to POSTGRES_USER (or hasura)
KEYCLOAK_DB_USER=keycloak
# Defaults to POSTGRES_PASSWORD
KEYCLOAK_DB_PASSWORD=keycloak-passwordFor production, use long random secrets.
Run: ./init.sh
The script is idempotent and does everything automatically:
- reads values from
.env - if a value is missing, tries to use defaults from
docker-compose.yml - starts
postgres, prepares optional metadata/keycloak DB users and databases, then startskeycloak, thenhasura-api - checks readiness of Keycloak and Hasura
- creates or updates Keycloak realm settings (including frontend URL)
- creates or updates Keycloak client settings (including redirect/web origins)
- creates/updates protocol mappers for Hasura claims:
x-hasura-default-rolex-hasura-allowed-rolesx-hasura-user-id
- creates/updates a test user and assigns the default role
- runs an integration smoke test:
- gets a real user access token from Keycloak
- executes GraphQL query in Hasura using
Authorization: Bearer <token>
Optional env vars for init:
KEYCLOAK_CLIENT_ID=frontend-app
KEYCLOAK_FRONTEND_ORIGIN=http://localhost:3000
KEYCLOAK_REALM_FRONTEND_URL=http://127.0.0.1:8082
KEYCLOAK_TEST_USERNAME=init-user
KEYCLOAK_TEST_PASSWORD=init-pass
HASURA_DEFAULT_ROLE=user
HASURA_API_URL=http://127.0.0.1:8080Hasura validates Bearer access tokens from Keycloak via HASURA_GRAPHQL_JWT_SECRET.
The compose file already configures:
jwk_url:http://keycloak:8080/realms/$KEYCLOAK_REALM/protocol/openid-connect/certsissuer:$KEYCLOAK_PUBLIC_URL/realms/$KEYCLOAK_REALMclaims_namespace_path:$(Hasura readsx-hasura-*claims from token root)
Realm/client/mappers are configured by ./init.sh from the Setup section.
If frontend URLs or realm settings change, run ./init.sh again.
Open:
http://127.0.0.1:8082/admin
Verify:
- realm exists
- client redirect URI and web origin match your frontend URL
- mappers for
x-hasura-*claims are present
In Hasura Console:
- Track tables
- Create permissions for role
user(and other roles if needed) - Use
x-hasura-user-idin row-level checks
Example filter:
{
"owner_id": {
"_eq": "X-Hasura-User-Id"
}
}Frontend authenticates in Keycloak, gets an access token, then calls Hasura:
import Keycloak from "keycloak-js";
const keycloak = new Keycloak({
url: "http://127.0.0.1:8082",
realm: "hasura",
clientId: "frontend-app"
});
await keycloak.init({ onLoad: "login-required", pkceMethod: "S256" });
const token = keycloak.token;
await fetch("http://127.0.0.1:8080/v1/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
query: "query { __typename }"
})
});Do not use x-hasura-admin-secret in frontend code.
Starts runtime services.
docker compose up -dCheck health:
curl http://127.0.0.1:8080/healthzGraphQL endpoint:
http://127.0.0.1:8080/v1/graphql
Create the project folder:
mkdir -p hasuraCreate hasura/config.yaml:
version: 3
endpoint: http://hasura-admin:8080
metadata_directory: metadata
migrations_directory: migrations
seeds_directory: seedsThe endpoint uses the Docker network name, not localhost.
Show CLI version:
docker compose --profile tools run --rm hasura-cli versionExport metadata:
docker compose --profile tools run --rm hasura-cli metadata exportReload metadata:
docker compose --profile tools run --rm hasura-cli metadata reloadCreate an empty migration:
docker compose --profile tools run --rm hasura-cli migrate create create_some_tablesEdit the generated files:
hasura/migrations/default/<timestamp>_create_some_tables/up.sql
hasura/migrations/default/<timestamp>_create_some_tables/down.sql
Example up.sql:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE public.users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text NOT NULL UNIQUE,
name text,
created_at timestamptz NOT NULL DEFAULT now()
);Example down.sql:
DROP TABLE IF EXISTS public.users;Apply locally:
docker compose --profile tools run --rm hasura-cli migrate apply --database-name defaultTrack the table in Console:
Data → Databases → default → Track
Then export metadata: docker compose --profile tools run --rm hasura-cli metadata export
Commit:
git add hasura
git commit -m "Add some table"Pull the new code.
Start admin mode only for deploy work:
docker compose up -d postgres hasura-adminApply migrations:
docker compose --profile tools run --rm hasura-cli migrate apply --database-name defaultApply metadata:
docker compose --profile tools run --rm hasura-cli metadata applyReload metadata:
docker compose --profile tools run --rm hasura-cli metadata reloadRestart the production API:
docker compose up -d hasura-apiStop admin mode after deploy (optional):
docker compose stop hasura-adminCreate backup:
docker compose exec postgres pg_dump \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
-Fc \
-f /tmp/app.dumpCopy backup to host:
docker compose cp postgres:/tmp/app.dump ./app.dumpCopy backup into container:
docker compose cp ./app.dump postgres:/tmp/app.dumpRestore:
docker compose exec postgres pg_restore \
-U "$POSTGRES_USER" \
-d "$POSTGRES_DB" \
--clean \
--if-exists \
/tmp/app.dumpKeep hasura-api as the only runtime endpoint.
Keep hasura-admin behind localhost, SSH, or VPN.
Do not expose metadata, config, or pgdump APIs to the public.
Use permissions for every role.
Use allowlist when the frontend queries are stable.
Disable query-log in production if requests may contain private data.
Recommended production value:
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log- Start production:
docker compose up -d - Start admin:
docker compose up -d postgres hasura-admin - Open admin console:
http://127.0.0.1:8081(useHASURA_GRAPHQL_ADMIN_SECRET) - Apply migrations:
docker compose --profile tools run --rm hasura-cli migrate apply --database-name default - Apply metadata:
docker compose --profile tools run --rm hasura-cli metadata apply - Reload metadata:
docker compose --profile tools run --rm hasura-cli metadata reload - Run auth e2e tests:
cd test && yarn test - Stop everything:
docker compose down - Stop and delete database volume:
docker compose down -v