Hasura GraphQL Engine Mastery
A comprehensive skill for building production-ready GraphQL APIs with Hasura. Master instant API generation, granular permissions, authentication integration, event-driven architectures, custom business logic, and remote schema stitching for modern applications.
When to Use This Skill
Use Hasura GraphQL Engine when:
-
Building GraphQL APIs rapidly without writing backend code
-
Need instant CRUD APIs from existing PostgreSQL databases
-
Implementing granular row-level and column-level security
-
Building real-time applications with GraphQL subscriptions
-
Integrating multiple data sources (databases, REST APIs, GraphQL services)
-
Creating event-driven architectures with database triggers
-
Extending GraphQL with custom business logic via Actions
-
Implementing authentication and authorization at the API layer
-
Building admin panels, dashboards, or internal tools quickly
-
Migrating from REST to GraphQL without rewriting backend
-
Needing production-ready features (caching, rate limiting, monitoring)
-
Building multi-tenant SaaS applications with role-based access
Core Concepts
Instant GraphQL API Generation
Hasura's primary value proposition is automatic GraphQL API generation from your database schema:
-
Table Tracking: Point Hasura at PostgreSQL tables to instantly get queries, mutations, and subscriptions
-
Relationship Detection: Automatically infers foreign key relationships as GraphQL connections
-
Type Safety: Database schema translates directly to GraphQL types
-
Zero Code: No resolver writing, no ORM configuration, no boilerplate
-
Real-time by Default: Every query automatically has a subscription counterpart
How it works:
-
Connect Hasura to your PostgreSQL database
-
Track tables in the Hasura Console
-
GraphQL API is immediately available with:
-
query
-
Fetch data with filtering, sorting, pagination
-
mutation
-
Insert, update, delete operations
-
subscription
-
Real-time data updates via WebSockets
Metadata-Driven Architecture
Hasura is metadata-driven, not code-driven:
-
Metadata: JSON/YAML configuration defining your API
-
Declarative: Define what you want, not how to implement it
-
Version Control: Metadata files can be committed to Git
-
CLI Migration: Hasura CLI manages metadata and migrations
-
Programmatic Control: Metadata API for automation
Key metadata components:
-
Table tracking and relationships
-
Permission rules
-
Remote schemas
-
Actions
-
Event triggers
-
Custom functions
Permission System
Hasura's permission system is its most powerful feature, enabling fine-grained access control:
-
Role-Based: Define permissions per GraphQL operation per role
-
Row-Level Security: Control which rows users can access
-
Column-Level Security: Hide sensitive columns from specific roles
-
Session Variables: Dynamic permissions based on JWT claims or webhook data
-
Check Constraints: Boolean expressions determining access
Permission Types:
-
select
-
Read permissions
-
insert
-
Create permissions
-
update
-
Modify permissions
-
delete
-
Remove permissions
Authentication Integration
Hasura delegates authentication to your auth service but handles authorization:
-
JWT Mode: Validate JWT tokens containing user claims
-
Webhook Mode: Call webhook to get session variables
-
Session Variables: x-hasura-role , x-hasura-user-id , custom claims
-
Multi-Provider: Support Auth0, Firebase, Cognito, custom auth
Auth Flow:
-
User authenticates with your auth service (Auth0, Firebase, custom)
-
Auth service issues JWT with Hasura claims
-
Client sends JWT in Authorization header
-
Hasura validates JWT and extracts session variables
-
Permissions evaluated using session variables
-
GraphQL query executed with appropriate access control
Event Triggers
Event Triggers enable event-driven architectures by invoking webhooks on database changes:
-
Database Events: INSERT, UPDATE, DELETE triggers
-
Reliable Delivery: At-least-once delivery with retries
-
Payload: Old and new row data in JSON
-
Async Processing: Long-running tasks, external integrations
-
Use Cases: Send emails, sync to Elasticsearch, update cache, trigger workflows
Actions
Actions extend Hasura with custom business logic:
-
Custom Mutations: Define GraphQL mutations handled by your code
-
Custom Queries: Add custom query logic beyond database access
-
REST Integration: Call REST APIs from GraphQL
-
Type Safety: Define input/output types in GraphQL SDL
-
Handler: Your HTTP endpoint receives GraphQL variables
Common use cases:
-
Payment processing
-
Complex validations
-
Third-party API calls
-
Custom algorithms
-
File uploads
-
Email sending
Remote Schemas
Remote Schemas enable schema stitching by merging external GraphQL APIs:
-
Schema Stitching: Unify multiple GraphQL services
-
Type Extension: Extend types with fields from remote schemas
-
Permissions: Apply role-based permissions to remote schemas
-
Namespace: Isolate remote schemas to avoid conflicts
-
Use Cases: Microservices, legacy GraphQL APIs, third-party services
Real-Time Subscriptions
Hasura provides native GraphQL subscriptions:
-
Live Queries: Automatically push updates when data changes
-
WebSocket Protocol: Efficient bi-directional communication
-
Multiplexing: Optimize subscriptions for many concurrent clients
-
Filtering: Subscribe to specific subsets of data
-
Polling Fallback: HTTP-based streaming for restricted networks
Permission System Deep Dive
Row-Level Security
Row-level security uses boolean check expressions to filter accessible rows:
Example: Users can only see their own data
{ "check": { "user_id": { "_eq": "X-Hasura-User-Id" } } }
Example: Multi-tenant data isolation
{ "check": { "tenant_id": { "_eq": "X-Hasura-Tenant-Id" } } }
Example: Complex access rules
{ "check": { "_or": [ { "user_id": { "_eq": "X-Hasura-User-Id" } }, { "is_public": { "_eq": true } } ] } }
Column-Level Security
Control which columns are visible per role:
Example: Hide sensitive user fields
select: columns: - id - username - email # password_hash is hidden # created_at is hidden
Example: Different views for different roles
Admin role sees all columns
select: columns: "*"
User role sees limited columns
select: columns: - id - username - profile_picture
Insert Permissions
Control what data can be inserted:
Example: Set user_id from session
{ "check": { "user_id": { "_eq": "X-Hasura-User-Id" } }, "set": { "user_id": "X-Hasura-User-Id" } }
Example: Validate ownership before insert
{ "check": { "project": { "owner_id": { "_eq": "X-Hasura-User-Id" } } } }
Update Permissions
Control which rows can be updated and what values can be set:
Example: Update own data only
{ "filter": { "user_id": { "_eq": "X-Hasura-User-Id" } }, "check": { "user_id": { "_eq": "X-Hasura-User-Id" } }, "set": { "updated_at": "now()" } }
filter: Which rows can be selected for update check: Validation after update completes set: Automatically set column values
Delete Permissions
Control which rows can be deleted:
Example: Delete own data only
{ "filter": { "user_id": { "_eq": "X-Hasura-User-Id" } } }
Authentication Integration
JWT Mode Configuration
Configure Hasura to validate JWT tokens:
Environment Variable:
HASURA_GRAPHQL_JWT_SECRET='{ "type": "RS256", "key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" }'
JWT Claims Structure:
{ "sub": "user123", "iat": 1633024800, "exp": 1633111200, "https://hasura.io/jwt/claims": { "x-hasura-default-role": "user", "x-hasura-allowed-roles": ["user", "admin"], "x-hasura-user-id": "user123", "x-hasura-org-id": "org456" } }
Required Claims:
-
x-hasura-default-role : Default role if not specified in request
-
x-hasura-allowed-roles : Array of roles user can assume
-
Custom claims like x-hasura-user-id for permission checks
Auth0 Integration
Auth0 Rule to add Hasura claims:
function (user, context, callback) { const namespace = "https://hasura.io/jwt/claims"; context.idToken[namespace] = { 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], 'x-hasura-user-id': user.user_id }; callback(null, user, context); }
Client usage:
const token = await auth0Client.getTokenSilently();
const response = await fetch('https://my-hasura.app/v1/graphql', {
method: 'POST',
headers: {
'Authorization': Bearer ${token},
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, variables })
});
Firebase Integration
Firebase custom claims:
// Admin SDK const admin = require('firebase-admin');
async function setCustomClaims(uid) { await admin.auth().setCustomUserClaims(uid, { 'https://hasura.io/jwt/claims': { 'x-hasura-default-role': 'user', 'x-hasura-allowed-roles': ['user'], 'x-hasura-user-id': uid } }); }
Webhook Mode
Alternative to JWT - Hasura calls your webhook for each request:
Webhook endpoint:
app.post('/auth-webhook', async (req, res) => { const authHeader = req.headers['authorization'];
// Validate token (your logic) const user = await validateToken(authHeader);
if (!user) { return res.status(401).json({ message: 'Unauthorized' }); }
// Return session variables res.json({ 'X-Hasura-User-Id': user.id, 'X-Hasura-Role': user.role, 'X-Hasura-Org-Id': user.orgId }); });
Hasura config:
HASURA_GRAPHQL_AUTH_HOOK=https://myapp.com/auth-webhook HASURA_GRAPHQL_AUTH_HOOK_MODE=POST
Event Triggers
Creating Event Triggers
Event triggers invoke webhooks on database changes:
Via Console:
-
Navigate to Events tab
-
Create Trigger
-
Select table and operations (INSERT, UPDATE, DELETE)
-
Provide webhook URL
-
Configure retry and timeout settings
Via Metadata API:
POST /v1/metadata HTTP/1.1 Content-Type: application/json X-Hasura-Role: admin
{ "type": "create_event_trigger", "args": { "name": "user_created", "table": { "name": "users", "schema": "public" }, "webhook": "https://myapp.com/webhooks/user-created", "insert": { "columns": "*" }, "retry_conf": { "num_retries": 3, "interval_sec": 10, "timeout_sec": 60 } } }
Event Payload Structure
Webhook receives structured JSON payload:
{ "event": { "session_variables": { "x-hasura-role": "user", "x-hasura-user-id": "123" }, "op": "INSERT", "data": { "old": null, "new": { "id": "uuid-here", "email": "user@example.com", "created_at": "2025-01-15T10:30:00Z" } } }, "created_at": "2025-01-15T10:30:00.123456Z", "id": "event-id", "trigger": { "name": "user_created" }, "table": { "schema": "public", "name": "users" } }
Event Trigger Use Cases
Send Welcome Email:
// Webhook handler app.post('/webhooks/user-created', async (req, res) => { const { event } = req.body; const user = event.data.new;
await sendEmail({ to: user.email, subject: 'Welcome!', template: 'welcome', data: { name: user.name } });
res.json({ success: true }); });
Sync to Elasticsearch:
app.post('/webhooks/product-updated', async (req, res) => { const { event } = req.body; const product = event.data.new;
await esClient.index({ index: 'products', id: product.id, body: product });
res.json({ success: true }); });
Trigger Workflow:
app.post('/webhooks/order-placed', async (req, res) => { const { event } = req.body; const order = event.data.new;
// Trigger payment processing await processPayment(order.id);
// Notify inventory system await updateInventory(order.items);
// Send confirmation email await sendOrderConfirmation(order);
res.json({ success: true }); });
Actions (Custom Business Logic)
Defining Actions
Actions extend GraphQL with custom mutations and queries:
GraphQL SDL Definition:
type Mutation { login(username: String!, password: String!): LoginResponse }
type LoginResponse { accessToken: String! refreshToken: String! user: User! }
Action Configuration:
- name: login
definition:
kind: synchronous
handler: https://myapp.com/actions/login
forward_client_headers: true
headers:
- name: X-API-Key
value: secret-key
permissions:
- role: anonymous
Action Handler Implementation
Express.js Handler:
app.post('/actions/login', async (req, res) => { const { input, session_variables } = req.body; const { username, password } = input;
// Validate credentials const user = await validateCredentials(username, password);
if (!user) { return res.status(401).json({ message: 'Invalid credentials' }); }
// Generate tokens const accessToken = generateJWT(user); const refreshToken = generateRefreshToken(user);
// Return action response res.json({ accessToken, refreshToken, user: { id: user.id, username: user.username, email: user.email } }); });
Action Permissions
Control which roles can execute actions:
Via Metadata API:
POST /v1/metadata HTTP/1.1 Content-Type: application/json X-Hasura-Role: admin
{ "type": "create_action_permission", "args": { "action": "insertAuthor", "role": "user" } }
Multiple Roles:
permissions:
- role: user
- role: admin
- role: anonymous
Action Types
Synchronous Actions:
-
Client waits for response
-
Use for: Login, payments, validations
-
Timeout: Configurable (default 30s)
Asynchronous Actions:
-
Returns immediately with action ID
-
Use for: Long-running tasks, batch processing
-
Poll for completion or use webhooks
Advanced Action Patterns
Payment Processing:
type Mutation { processPayment( orderId: ID! amount: Float! currency: String! paymentMethod: String! ): PaymentResponse }
type PaymentResponse { success: Boolean! transactionId: String error: String }
File Upload:
type Mutation { uploadFile( file: String! # Base64 encoded fileName: String! mimeType: String! ): FileUploadResponse }
type FileUploadResponse { url: String! fileId: ID! }
Complex Validation:
type Mutation { createProject( name: String! description: String! teamMembers: [ID!]! ): CreateProjectResponse }
type CreateProjectResponse { project: Project errors: [ValidationError!] }
type ValidationError { field: String! message: String! }
Remote Schemas
Adding Remote Schemas
Integrate external GraphQL APIs:
Via Metadata API:
POST /v1/metadata HTTP/1.1 Content-Type: application/json X-Hasura-Role: admin
{ "type": "add_remote_schema", "args": { "name": "auth0_api", "definition": { "url": "https://myapp.auth0.com/graphql", "headers": [ { "name": "Authorization", "value": "Bearer ${AUTH0_TOKEN}" } ], "forward_client_headers": false, "timeout_seconds": 60 } } }
Remote Schema Customization
Customize type and field names to avoid conflicts:
{ "type": "add_remote_schema", "args": { "name": "countries", "definition": { "url": "https://countries.trevorblades.com/graphql", "customization": { "root_fields_namespace": "countries_api", "type_names": { "prefix": "Countries_", "suffix": "Type" }, "field_names": [ { "parent_type": "Country", "prefix": "country" } ] } } } }
Remote Schema Permissions
Apply role-based permissions to remote schemas:
Original Remote Schema:
type User { id: ID! first_name: String! last_name: String! phone: String! email: String! }
type Query { user(id: ID!): User get_users_by_name(first_name: String!, last_name: String): [User] }
Restricted Schema for 'public' Role:
type User { first_name: String! last_name: String! }
type Query { get_users_by_name(first_name: String!, last_name: String): [User] }
Via Metadata API:
POST /v1/metadata HTTP/1.1 Content-Type: application/json X-Hasura-Role: admin
{ "type": "add_remote_schema_permissions", "args": { "remote_schema": "user_api", "role": "public", "definition": { "schema": "type User { first_name: String! last_name: String! } type Query { get_users_by_name(first_name: String!, last_name: String): [User] }" } } }
Remote Schema Argument Presets
Automatically inject session variables into remote schema queries:
Session Variable Preset:
type Query { get_user(id: ID! @preset(value: "x-hasura-user-id")): User get_user_activities(user_id: ID!, limit: Int!): [Activity] }
Static Value Preset:
type Query { get_user(id: ID! @preset(value: "x-hasura-user-id")): User get_user_activities( user_id: ID! limit: Int! @preset(value: 10) ): [Activity] }
Literal String (not session variable):
type Query { hello(text: String! @preset(value: "x-hasura-hello", static: true)) }
Remote Relationships
Connect local database tables to remote schemas:
Example: Link local customer to remote payments API
SQL Table:
CREATE TABLE customer ( id SERIAL PRIMARY KEY, name TEXT NOT NULL );
Remote Schema (Payments API):
type Transaction { customer_id: Int! amount: Int! time: String! merchant: String! }
type Query { transactions(customer_id: String!, limit: Int): [Transaction] }
Remote Relationship Definition:
- table:
name: customer
schema: public
remote_relationships:
- name: customer_transactions_history definition: remote_schema: payments hasura_fields: - id remote_field: transactions: arguments: customer_id: $id
GraphQL Query with Remote Relationship:
query { customer { name customer_transactions_history { amount time } } }
Production Deployment
Docker Deployment
docker-compose.yml:
version: '3.8'
services: postgres: image: postgres:15 restart: always volumes: - db_data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: postgrespassword healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5
hasura: image: hasura/graphql-engine:v2.36.0 ports: - "8080:8080" depends_on: postgres: condition: service_healthy restart: always environment: HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres HASURA_GRAPHQL_ENABLE_CONSOLE: "true" HASURA_GRAPHQL_DEV_MODE: "true" HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"super-secret-jwt-signing-key-min-32-chars"}' HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous
volumes: db_data:
Kubernetes Deployment
hasura-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: hasura namespace: production spec: replicas: 3 selector: matchLabels: app: hasura template: metadata: labels: app: hasura spec: containers: - name: hasura image: hasura/graphql-engine:v2.36.0 ports: - containerPort: 8080 env: - name: HASURA_GRAPHQL_DATABASE_URL valueFrom: secretKeyRef: name: hasura-secrets key: database-url - name: HASURA_GRAPHQL_ADMIN_SECRET valueFrom: secretKeyRef: name: hasura-secrets key: admin-secret - name: HASURA_GRAPHQL_JWT_SECRET valueFrom: secretKeyRef: name: hasura-secrets key: jwt-secret - name: HASURA_GRAPHQL_ENABLE_CONSOLE value: "false" - name: HASURA_GRAPHQL_ENABLE_TELEMETRY value: "false" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5
apiVersion: v1 kind: Service metadata: name: hasura namespace: production spec: type: ClusterIP selector: app: hasura ports:
- port: 80 targetPort: 8080
Environment Variables (Production)
Essential Production Config:
Database
HASURA_GRAPHQL_DATABASE_URL=postgres://user:password@host:5432/dbname
Security
HASURA_GRAPHQL_ADMIN_SECRET=strong-random-secret HASURA_GRAPHQL_JWT_SECRET='{"type":"RS256","key":"..."}' HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous
Performance
HASURA_GRAPHQL_ENABLE_CONSOLE=false HASURA_GRAPHQL_DEV_MODE=false HASURA_GRAPHQL_ENABLE_TELEMETRY=false
Logging
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log
Rate Limiting
HASURA_GRAPHQL_RATE_LIMIT_PER_MINUTE=1000
CORS
HASURA_GRAPHQL_CORS_DOMAIN=https://myapp.com,https://admin.myapp.com
Connections
HASURA_GRAPHQL_PG_CONNECTIONS=50 HASURA_GRAPHQL_PG_TIMEOUT=60
Monitoring and Observability
Health Check Endpoint:
curl http://hasura:8080/healthz
Returns: OK
Prometheus Metrics:
HASURA_GRAPHQL_ENABLE_METRICS=true HASURA_GRAPHQL_METRICS_SECRET=metrics-secret
Access at: http://hasura:8080/v1/metrics
Structured Logging:
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log,query-log HASURA_GRAPHQL_LOG_LEVEL=info
APM Integration (Datadog example):
env:
- name: HASURA_GRAPHQL_ENABLE_APM value: "true"
- name: DD_AGENT_HOST valueFrom: fieldRef: fieldPath: status.hostIP
- name: DD_SERVICE value: "hasura-graphql"
- name: DD_ENV value: "production"
Migrations and Version Control
Hasura CLI Setup
Initialize Hasura project:
hasura init my-project --endpoint https://hasura.myapp.com cd my-project
Project structure:
my-project/ ├── config.yaml # Hasura CLI config ├── metadata/ # Metadata files │ ├── databases/ │ │ └── default/ │ │ ├── tables/ │ │ │ ├── public_users.yaml │ │ │ └── public_posts.yaml │ ├── actions.yaml │ ├── remote_schemas.yaml │ └── version.yaml └── migrations/ # Database migrations └── default/ ├── 1642531200000_create_users_table/ │ └── up.sql └── 1642531300000_create_posts_table/ └── up.sql
Creating Migrations
Via Console (auto-tracked):
Start console with migration tracking
hasura console
Make changes in console UI
Migrations auto-generated in migrations/ folder
Manual migration:
Create migration
hasura migrate create create_users_table --database-name default
Edit generated SQL files
migrations/default/{timestamp}_create_users_table/up.sql
migrations/default/{timestamp}_create_users_table/down.sql
Example migration (up.sql):
CREATE TABLE public.users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() );
CREATE INDEX idx_users_email ON public.users(email); CREATE INDEX idx_users_username ON public.users(username);
Example migration (down.sql):
DROP TABLE IF EXISTS public.users CASCADE;
Applying Migrations
Apply migrations:
Apply all pending migrations
hasura migrate apply --database-name default
Apply specific version
hasura migrate apply --version 1642531200000 --database-name default
Check migration status
hasura migrate status --database-name default
Exporting and Importing Metadata
Export metadata:
hasura metadata export
Exports to metadata/ folder
Apply metadata:
hasura metadata apply
Applies metadata from metadata/ folder
Reload metadata:
hasura metadata reload
CI/CD Integration
GitHub Actions example:
name: Deploy Hasura
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2
- name: Install Hasura CLI
run: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
- name: Apply Migrations
env:
HASURA_GRAPHQL_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
HASURA_GRAPHQL_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}
run: |
cd hasura
hasura migrate apply --database-name default
hasura metadata apply
- name: Reload Metadata
env:
HASURA_GRAPHQL_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
HASURA_GRAPHQL_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}
run: |
cd hasura
hasura metadata reload
Best Practices
Security Best Practices
Always use ADMIN_SECRET in production
-
Never expose admin API without authentication
-
Rotate secrets regularly
-
Use strong, random secrets (min 32 characters)
Implement proper JWT validation
-
Use RS256 (asymmetric) in production
-
Set appropriate token expiration
-
Validate issuer and audience claims
Apply least-privilege permissions
-
Start with no access, add permissions as needed
-
Use row-level security for all tables
-
Hide sensitive columns from unauthorized roles
Disable console in production
-
HASURA_GRAPHQL_ENABLE_CONSOLE=false
-
Use metadata files and CLI for changes
Enable rate limiting
-
Protect against DoS attacks
-
Set per-role limits if needed
-
Monitor and adjust based on usage
Validate webhook payloads
-
Use webhook secrets for event triggers
-
Validate action inputs
-
Sanitize all user inputs
Performance Best Practices
Optimize database queries
-
Create appropriate indexes
-
Use database views for complex queries
-
Leverage PostgreSQL performance tuning
Use query caching
-
Enable @cached directive for expensive queries
-
Set appropriate TTL values
-
Cache at CDN level when possible
Limit query depth and complexity
-
Set max query depth limits
-
Restrict deeply nested queries
-
Use pagination for large result sets
Configure connection pooling
-
Tune HASURA_GRAPHQL_PG_CONNECTIONS
-
Monitor connection usage
-
Use PgBouncer for large deployments
Optimize subscriptions
-
Use subscription multiplexing
-
Limit concurrent subscriptions per client
-
Consider polling for less time-sensitive data
Development Workflow Best Practices
Version control metadata
-
Commit metadata/ folder to Git
-
Use migrations for all schema changes
-
Review metadata changes in PRs
Environment separation
-
Development, staging, production environments
-
Use different admin secrets per environment
-
Test migrations in staging first
Testing strategy
-
Test permissions thoroughly
-
Integration test event triggers
-
Test action handlers independently
Documentation
-
Document custom actions and their inputs/outputs
-
Explain complex permission rules
-
Maintain API documentation for consumers
Monitoring and alerting
-
Monitor query performance
-
Alert on failed webhooks/event triggers
-
Track error rates and latencies
Common Patterns and Examples
Pattern 1: Multi-Tenant SaaS
Schema:
CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL );
CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, organization_id UUID NOT NULL REFERENCES organizations(id) );
CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, organization_id UUID NOT NULL REFERENCES organizations(id) );
Permissions (users table):
{ "filter": { "organization_id": { "_eq": "X-Hasura-Org-Id" } } }
JWT Claims:
{ "https://hasura.io/jwt/claims": { "x-hasura-default-role": "user", "x-hasura-allowed-roles": ["user", "org-admin"], "x-hasura-user-id": "user-uuid", "x-hasura-org-id": "org-uuid" } }
Pattern 2: Social Media Application
Schema:
CREATE TABLE users ( id UUID PRIMARY KEY, username TEXT UNIQUE NOT NULL, bio TEXT, avatar_url TEXT );
CREATE TABLE posts ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), content TEXT NOT NULL, is_public BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() );
CREATE TABLE follows ( follower_id UUID REFERENCES users(id), following_id UUID REFERENCES users(id), PRIMARY KEY (follower_id, following_id) );
CREATE TABLE likes ( user_id UUID REFERENCES users(id), post_id UUID REFERENCES posts(id), PRIMARY KEY (user_id, post_id) );
Permission: View posts (user can see own posts, public posts, and posts from followed users):
{ "filter": { "_or": [ { "user_id": { "_eq": "X-Hasura-User-Id" } }, { "is_public": { "_eq": true } }, { "user": { "followers": { "follower_id": { "_eq": "X-Hasura-User-Id" } } } } ] } }
Pattern 3: E-Commerce Platform
Schema:
CREATE TABLE products ( id UUID PRIMARY KEY, name TEXT NOT NULL, price DECIMAL(10,2) NOT NULL, stock_quantity INT NOT NULL, is_active BOOLEAN DEFAULT true );
CREATE TABLE orders ( id UUID PRIMARY KEY, user_id UUID NOT NULL, status TEXT NOT NULL, total DECIMAL(10,2) NOT NULL, created_at TIMESTAMP DEFAULT NOW() );
CREATE TABLE order_items ( id UUID PRIMARY KEY, order_id UUID REFERENCES orders(id), product_id UUID REFERENCES products(id), quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL );
Event Trigger: Order confirmation email
app.post('/webhooks/order-created', async (req, res) => { const { event } = req.body; const order = event.data.new;
// Fetch order details with items const orderDetails = await fetchOrderDetails(order.id);
// Send confirmation email await sendEmail({ to: orderDetails.user.email, template: 'order-confirmation', data: orderDetails });
res.json({ success: true }); });
Action: Process payment
type Mutation { processPayment( orderId: ID! paymentMethodId: String! ): PaymentResponse }
type PaymentResponse { success: Boolean! orderId: ID! transactionId: String error: String }
Pattern 4: Real-Time Collaboration
Schema:
CREATE TABLE documents ( id UUID PRIMARY KEY, title TEXT NOT NULL, content JSONB NOT NULL DEFAULT '{}'::jsonb, owner_id UUID NOT NULL, updated_at TIMESTAMP DEFAULT NOW() );
CREATE TABLE document_collaborators ( document_id UUID REFERENCES documents(id), user_id UUID NOT NULL, permission TEXT NOT NULL, -- 'read', 'write', 'admin' PRIMARY KEY (document_id, user_id) );
Permission: Access documents (own or collaborated):
{ "filter": { "_or": [ { "owner_id": { "_eq": "X-Hasura-User-Id" } }, { "collaborators": { "user_id": { "_eq": "X-Hasura-User-Id" } } } ] } }
GraphQL Subscription: Real-time updates
subscription DocumentUpdates($documentId: uuid!) { documents_by_pk(id: $documentId) { id title content updated_at } }
Pattern 5: Admin Dashboard with Analytics
Custom SQL Function for analytics:
CREATE OR REPLACE FUNCTION get_user_stats(user_row users) RETURNS TABLE ( total_posts INT, total_followers INT, total_following INT, engagement_rate DECIMAL ) AS $$ SELECT (SELECT COUNT() FROM posts WHERE user_id = user_row.id)::INT, (SELECT COUNT() FROM follows WHERE following_id = user_row.id)::INT, (SELECT COUNT(*) FROM follows WHERE follower_id = user_row.id)::INT, (SELECT AVG(like_count) FROM posts WHERE user_id = user_row.id)::DECIMAL $$ LANGUAGE SQL STABLE;
Track function in Hasura:
- function: name: get_user_stats schema: public configuration: custom_root_fields: function: getUserStats
GraphQL Query:
query UserWithStats { users { id username get_user_stats { total_posts total_followers total_following engagement_rate } } }
Troubleshooting
Common Issues and Solutions
Issue: JWT validation failing
Solution:
- Verify JWT secret configuration matches your auth provider
- Check JWT contains required Hasura claims
- Ensure claims are in correct namespace (https://hasura.io/jwt/claims)
- Validate JWT hasn't expired
- Check issuer and audience if configured
Issue: Permission denied errors
Solution:
- Check role is in allowed_roles
- Verify permission rules allow the operation
- Test with admin role to isolate permission issue
- Check session variables are being sent correctly
- Review both row-level and column-level permissions
Issue: Event trigger not firing
Solution:
- Check webhook is accessible from Hasura
- Verify table name and operation match trigger config
- Check webhook returns 200 status
- Review event trigger logs in Hasura console
- Ensure database triggers are enabled
Issue: Action returning errors
Solution:
- Verify action handler URL is accessible
- Check request/response format matches action definition
- Review action handler logs
- Test action handler independently
- Verify permissions allow the role to execute action
Issue: Remote schema not loading
Solution:
- Verify remote GraphQL endpoint is accessible
- Check authentication headers if required
- Test remote schema independently
- Review timeout settings
- Check for type name conflicts
Issue: Subscription connection dropping
Solution:
- Check WebSocket support on hosting platform
- Verify connection timeout settings
- Implement reconnection logic in client
- Check for firewall/proxy blocking WebSockets
- Monitor connection pool limits
Additional Resources
Official Documentation
-
Hasura Docs: https://hasura.io/docs
-
Hasura GraphQL API Reference: https://hasura.io/docs/latest/api-reference
-
Hasura Cloud: https://hasura.io/cloud
Learning Resources
-
Hasura Learn: https://hasura.io/learn
-
Hasura Blog: https://hasura.io/blog
-
Hasura YouTube: https://youtube.com/hasurahq
Community
-
Discord: https://discord.gg/hasura
Tools and Integrations
-
Hasura CLI: https://hasura.io/docs/latest/hasura-cli/overview
-
Hasura Cloud Console: https://cloud.hasura.io
-
GraphQL Code Generator: https://www.graphql-code-generator.com
Skill Version: 1.0.0 Last Updated: January 2025 Skill Category: Backend, GraphQL, API Development, Real-time, Database Compatible With: PostgreSQL, Auth0, Firebase, Cognito, Kubernetes, Docker