Loading...
Loading...
Complete guide for Hasura GraphQL Engine including instant GraphQL APIs, permissions, authentication, event triggers, actions, and production deployment
npx skill4agent add manutej/luxor-claude-marketplace hasura-graphql-enginequerymutationsubscriptionselectinsertupdatedeletex-hasura-rolex-hasura-user-id{
"check": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}{
"check": {
"tenant_id": {
"_eq": "X-Hasura-Tenant-Id"
}
}
}{
"check": {
"_or": [
{
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
{
"is_public": {
"_eq": true
}
}
]
}
}select:
columns:
- id
- username
- email
# password_hash is hidden
# created_at is hidden# Admin role sees all columns
select:
columns: "*"
# User role sees limited columns
select:
columns:
- id
- username
- profile_picture{
"check": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
"set": {
"user_id": "X-Hasura-User-Id"
}
}{
"check": {
"project": {
"owner_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}{
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
"check": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
"set": {
"updated_at": "now()"
}
}{
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}HASURA_GRAPHQL_JWT_SECRET='{
"type": "RS256",
"key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}'{
"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"
}
}x-hasura-default-rolex-hasura-allowed-rolesx-hasura-user-idfunction (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);
}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 })
});// 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
}
});
}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_GRAPHQL_AUTH_HOOK=https://myapp.com/auth-webhook
HASURA_GRAPHQL_AUTH_HOOK_MODE=POSTPOST /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": {
"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"
}
}// 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 });
});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 });
});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 });
});type Mutation {
login(username: String!, password: String!): LoginResponse
}
type LoginResponse {
accessToken: String!
refreshToken: String!
user: User!
}- 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: anonymousapp.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
}
});
});POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "create_action_permission",
"args": {
"action": "insertAuthor",
"role": "user"
}
}permissions:
- role: user
- role: admin
- role: anonymoustype Mutation {
processPayment(
orderId: ID!
amount: Float!
currency: String!
paymentMethod: String!
): PaymentResponse
}
type PaymentResponse {
success: Boolean!
transactionId: String
error: String
}type Mutation {
uploadFile(
file: String! # Base64 encoded
fileName: String!
mimeType: String!
): FileUploadResponse
}
type FileUploadResponse {
url: String!
fileId: ID!
}type Mutation {
createProject(
name: String!
description: String!
teamMembers: [ID!]!
): CreateProjectResponse
}
type CreateProjectResponse {
project: Project
errors: [ValidationError!]
}
type ValidationError {
field: String!
message: String!
}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
}
}
}{
"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_"
}
]
}
}
}
}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]
}type User {
first_name: String!
last_name: String!
}
type Query {
get_users_by_name(first_name: String!, last_name: String): [User]
}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] }"
}
}
}type Query {
get_user(id: ID! @preset(value: "x-hasura-user-id")): User
get_user_activities(user_id: ID!, limit: Int!): [Activity]
}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]
}type Query {
hello(text: String! @preset(value: "x-hasura-hello", static: true))
}CREATE TABLE customer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);type Transaction {
customer_id: Int!
amount: Int!
time: String!
merchant: String!
}
type Query {
transactions(customer_id: String!, limit: Int): [Transaction]
}- table:
name: customer
schema: public
remote_relationships:
- name: customer_transactions_history
definition:
remote_schema: payments
hasura_fields:
- id
remote_field:
transactions:
arguments:
customer_id: $idquery {
customer {
name
customer_transactions_history {
amount
time
}
}
}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: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# 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=60curl http://hasura:8080/healthz
# Returns: OKHASURA_GRAPHQL_ENABLE_METRICS=true
HASURA_GRAPHQL_METRICS_SECRET=metrics-secret
# Access at: http://hasura:8080/v1/metricsHASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log,query-log
HASURA_GRAPHQL_LOG_LEVEL=infoenv:
- 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"hasura init my-project --endpoint https://hasura.myapp.com
cd my-projectmy-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# Start console with migration tracking
hasura console
# Make changes in console UI
# Migrations auto-generated in migrations/ folder# 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.sqlCREATE 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);DROP TABLE IF EXISTS public.users CASCADE;# 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 defaulthasura metadata export
# Exports to metadata/ folderhasura metadata apply
# Applies metadata from metadata/ folderhasura metadata reloadname: 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 reloadHASURA_GRAPHQL_ENABLE_CONSOLE=falseHASURA_GRAPHQL_PG_CONNECTIONSCREATE 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)
);{
"filter": {
"organization_id": {
"_eq": "X-Hasura-Org-Id"
}
}
}{
"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"
}
}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)
);{
"filter": {
"_or": [
{
"user_id": {
"_eq": "X-Hasura-User-Id"
}
},
{
"is_public": {
"_eq": true
}
},
{
"user": {
"followers": {
"follower_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}
]
}
}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
);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 });
});type Mutation {
processPayment(
orderId: ID!
paymentMethodId: String!
): PaymentResponse
}
type PaymentResponse {
success: Boolean!
orderId: ID!
transactionId: String
error: String
}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)
);{
"filter": {
"_or": [
{
"owner_id": {
"_eq": "X-Hasura-User-Id"
}
},
{
"collaborators": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
]
}
}subscription DocumentUpdates($documentId: uuid!) {
documents_by_pk(id: $documentId) {
id
title
content
updated_at
}
}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;- function:
name: get_user_stats
schema: public
configuration:
custom_root_fields:
function: getUserStatsquery UserWithStats {
users {
id
username
get_user_stats {
total_posts
total_followers
total_following
engagement_rate
}
}
}Solution:
1. Verify JWT secret configuration matches your auth provider
2. Check JWT contains required Hasura claims
3. Ensure claims are in correct namespace (https://hasura.io/jwt/claims)
4. Validate JWT hasn't expired
5. Check issuer and audience if configuredSolution:
1. Check role is in allowed_roles
2. Verify permission rules allow the operation
3. Test with admin role to isolate permission issue
4. Check session variables are being sent correctly
5. Review both row-level and column-level permissionsSolution:
1. Check webhook is accessible from Hasura
2. Verify table name and operation match trigger config
3. Check webhook returns 200 status
4. Review event trigger logs in Hasura console
5. Ensure database triggers are enabledSolution:
1. Verify action handler URL is accessible
2. Check request/response format matches action definition
3. Review action handler logs
4. Test action handler independently
5. Verify permissions allow the role to execute actionSolution:
1. Verify remote GraphQL endpoint is accessible
2. Check authentication headers if required
3. Test remote schema independently
4. Review timeout settings
5. Check for type name conflictsSolution:
1. Check WebSocket support on hosting platform
2. Verify connection timeout settings
3. Implement reconnection logic in client
4. Check for firewall/proxy blocking WebSockets
5. Monitor connection pool limits