A development Hasura and a production Hasura differ by a handful of settings, and the gap between them is the difference between an API and an open database. This is the checklist.
Turn the console off
HASURA_GRAPHQL_ENABLE_CONSOLE: "false"
HASURA_GRAPHQL_DEV_MODE: "false"The console is served by the engine itself and it is an administrative interface on your public endpoint. It requires the admin secret, so this is defence in depth rather than the only lock — but there is no reason to serve it, and running the console locally against a production endpoint through the CLI is the better workflow anyway.
DEV_MODE is the more urgent one. With it on, errors include internal detail —
including generated SQL — in the response. That is a schema disclosure you do not want, and it
is on by default in most compose files people copy.
Restrict introspection
Introspection lets a client download your entire schema. For a public API that is a feature; for a private one it is a map.
{
"type": "set_graphql_schema_introspection_options",
"args": {"disabled_for_roles": ["anonymous", "customer"]}
}Be honest about what this buys. It is obfuscation, not access control — permissions are what stop somebody reading data, and a determined attacker can guess field names. Disable it because there is no reason to publish the map, not because it protects you.
Look after the admin secret
It bypasses every rule you have written. Practical rules:
Never in a client. Not in a frontend bundle, not in a mobile app, not in a “temporary” debug build. Anything shipped to a device is public.
From a secret store, not a compose file. Development fixtures checked in on purpose are fine and should be obviously throwaway; production values come from the environment.
Rotate it. It is a single static string with unrestricted access and no expiry.
Hasura also supports scoped admin-secret alternatives in its paid tiers, but on Community Edition this one string is the whole story — treat it accordingly.
Least privilege at the database
The control people most often skip. Hasura connects as a Postgres user, and if that user is a superuser then a metadata mistake has no floor under it.
CREATE USER hasura_app WITH PASSWORD '...';
GRANT USAGE ON SCHEMA public TO hasura_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO hasura_app;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO hasura_app;Hasura needs its own schema for event-trigger bookkeeping, so give it one and let it own that. What it should not have is DDL rights on your application schema, unless you have deliberately chosen the Hasura-owns-migrations model from lesson 13.
The unauthorized role is a decision
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymousSet, every unauthenticated request runs as that role. Unset, they are refused. Both are correct in different products — the mistake is inheriting the setting without deciding.
If you do set it, audit that role’s permissions specifically and regularly. It is the one role every visitor on the internet gets for free, and it is the one people forget when adding a table — a new select permission granted to “all roles” includes this one.
CORS
HASURA_GRAPHQL_CORS_DOMAIN: "https://app.example.com, https://admin.example.com"Default is *. Narrow it. CORS does not stop a server-side attacker — it is a
browser control — but leaving it open removes a barrier for no benefit.
Allow-lists and rate limiting
Community Edition. The engine this track runs against reports server_type: ce. The features named below as Enterprise or Cloud were not demonstrated here — they are described from the documentation and labelled, rather than shown running.
An allow-list restricts the endpoint to a set of approved query documents. Anything else is rejected, which converts your GraphQL API into something closer to a fixed set of operations — a strong control for a first-party app where you ship the queries yourself.
Rate limiting and API limits (depth, node count, request size) cap what a single role or client can ask for.
Both are Enterprise and Cloud features. On Community Edition, the equivalents live in front of the engine: rate limiting at your ingress or CDN, and RESTified endpoints from lesson 14 as a poor-man’s allow-list, since they pin the query text.
Security in v3 (DDN)
On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.
Two changes stand out.
There is no admin secret. The single most powerful credential in a v2 deployment
does not exist in DDN — API access uses a Cloud PAT sent as a cloud_pat header.
Most of the section above about protecting and rotating that secret becomes a different problem
rather than a solved one.
Allow-lists move to plugins. They remain available but are implemented differently, as are caching and RESTified endpoints. API limits are marked work in progress, so if rate limiting is a control you rely on, check its status before migrating.
The permission model itself — the thing actually keeping data safe — carries over
intact as TypePermissions and ModelPermissions.
Next
Lesson 16 covers knowing what your API is doing once real traffic arrives.