
Hasura Graphql Engine
- 368 installs
- 61 repo stars
- Updated June 13, 2026
- manutej/luxor-claude-marketplace
Stand up Hasura GraphQL Engine over Postgres to expose real-time APIs, permissions, and relationships without hand-writing every resolver.
About
This skill guides Claude through Hasura GraphQL Engine setup and configuration: connecting Postgres, modeling relationships, enforcing role-based permissions, extending APIs with actions and remote schemas, and using subscriptions and event triggers for real-time data access patterns.
- Postgres schema tracking in Hasura
- Row-level permissions and roles
- Relationships and nested queries
- Remote schemas and actions
- Event triggers and real-time subscriptions
Hasura Graphql Engine by the numbers
- 368 all-time installs (skills.sh)
- +18 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #155 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill hasura-graphql-engineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 368 |
|---|---|
| repo stars | ★ 61 |
| Last updated | June 13, 2026 |
| Repository | manutej/luxor-claude-marketplace ↗ |
What it does
Stand up Hasura GraphQL Engine over Postgres to expose real-time APIs, permissions, and relationships without hand-writing every resolver.
Files
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: 1. Connect Hasura to your PostgreSQL database 2. Track tables in the Hasura Console 3. GraphQL API is immediately available with:
query- Fetch data with filtering, sorting, paginationmutation- Insert, update, delete operationssubscription- 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 permissionsinsert- Create permissionsupdate- Modify permissionsdelete- 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: 1. User authenticates with your auth service (Auth0, Firebase, custom) 2. Auth service issues JWT with Hasura claims 3. Client sends JWT in Authorization header 4. Hasura validates JWT and extracts session variables 5. Permissions evaluated using session variables 6. 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 hiddenExample: Different views for different roles
# Admin role sees all columns
select:
columns: "*"
# User role sees limited columns
select:
columns:
- id
- username
- profile_pictureInsert 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 requestx-hasura-allowed-roles: Array of roles user can assume- Custom claims like
x-hasura-user-idfor 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=POSTEvent Triggers
Creating Event Triggers
Event triggers invoke webhooks on database changes:
Via Console: 1. Navigate to Events tab 2. Create Trigger 3. Select table and operations (INSERT, UPDATE, DELETE) 4. Provide webhook URL 5. 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: anonymousAction 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: anonymousAction 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: $idGraphQL 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: 8080Environment 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=60Monitoring and Observability
Health Check Endpoint:
curl http://hasura:8080/healthz
# Returns: OKPrometheus Metrics:
HASURA_GRAPHQL_ENABLE_METRICS=true
HASURA_GRAPHQL_METRICS_SECRET=metrics-secret
# Access at: http://hasura:8080/v1/metricsStructured Logging:
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log,query-log
HASURA_GRAPHQL_LOG_LEVEL=infoAPM 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-projectProject 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.sqlCreating Migrations
Via Console (auto-tracked):
# Start console with migration tracking
hasura console
# Make changes in console UI
# Migrations auto-generated in migrations/ folderManual 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.sqlExample 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 defaultExporting and Importing Metadata
Export metadata:
hasura metadata export
# Exports to metadata/ folderApply metadata:
hasura metadata apply
# Applies metadata from metadata/ folderReload metadata:
hasura metadata reloadCI/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 reloadBest Practices
Security Best Practices
1. Always use ADMIN_SECRET in production
- Never expose admin API without authentication
- Rotate secrets regularly
- Use strong, random secrets (min 32 characters)
2. Implement proper JWT validation
- Use RS256 (asymmetric) in production
- Set appropriate token expiration
- Validate issuer and audience claims
3. 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
4. Disable console in production
HASURA_GRAPHQL_ENABLE_CONSOLE=false- Use metadata files and CLI for changes
5. Enable rate limiting
- Protect against DoS attacks
- Set per-role limits if needed
- Monitor and adjust based on usage
6. Validate webhook payloads
- Use webhook secrets for event triggers
- Validate action inputs
- Sanitize all user inputs
Performance Best Practices
1. Optimize database queries
- Create appropriate indexes
- Use database views for complex queries
- Leverage PostgreSQL performance tuning
2. Use query caching
- Enable @cached directive for expensive queries
- Set appropriate TTL values
- Cache at CDN level when possible
3. Limit query depth and complexity
- Set max query depth limits
- Restrict deeply nested queries
- Use pagination for large result sets
4. Configure connection pooling
- Tune
HASURA_GRAPHQL_PG_CONNECTIONS - Monitor connection usage
- Use PgBouncer for large deployments
5. Optimize subscriptions
- Use subscription multiplexing
- Limit concurrent subscriptions per client
- Consider polling for less time-sensitive data
Development Workflow Best Practices
1. Version control metadata
- Commit metadata/ folder to Git
- Use migrations for all schema changes
- Review metadata changes in PRs
2. Environment separation
- Development, staging, production environments
- Use different admin secrets per environment
- Test migrations in staging first
3. Testing strategy
- Test permissions thoroughly
- Integration test event triggers
- Test action handlers independently
4. Documentation
- Document custom actions and their inputs/outputs
- Explain complex permission rules
- Maintain API documentation for consumers
5. 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: getUserStatsGraphQL 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:
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 configuredIssue: Permission denied errors
Solution:
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 permissionsIssue: Event trigger not firing
Solution:
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 enabledIssue: Action returning errors
Solution:
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 actionIssue: Remote schema not loading
Solution:
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 conflictsIssue: Subscription connection dropping
Solution:
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 limitsAdditional 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
- GitHub: https://github.com/hasura/graphql-engine
- Forum: https://github.com/hasura/graphql-engine/discussions
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
Hasura GraphQL Engine Examples
Comprehensive collection of practical examples demonstrating Hasura's core features and real-world use cases.
Table of Contents
1. Basic CRUD Operations 2. Multi-Tenant SaaS Application 3. Social Media Platform 4. E-Commerce Platform 5. Real-Time Collaboration App 6. User Authentication System 7. Event-Driven Order Processing 8. Custom Payment Action 9. Remote Schema Integration 10. Advanced Permissions Patterns 11. Real-Time Analytics Dashboard 12. File Upload with Actions 13. GraphQL Query Optimization 14. Automated Email Notifications 15. Admin Panel with Row-Level Security 16. API Gateway Pattern 17. Metadata API Automation
---
Example 1: Basic CRUD Operations
Database Schema
-- Create users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Create index for faster lookups
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- Track table in Hasura via Console or Metadata APIGraphQL Queries
# 1. Fetch all users
query GetAllUsers {
users {
id
email
username
full_name
created_at
}
}
# 2. Fetch user by ID
query GetUserById($userId: uuid!) {
users_by_pk(id: $userId) {
id
email
username
full_name
avatar_url
}
}
# 3. Search users by username
query SearchUsers($searchTerm: String!) {
users(
where: { username: { _ilike: $searchTerm } }
limit: 10
) {
id
username
full_name
}
}
# 4. Paginated user list
query PaginatedUsers($limit: Int!, $offset: Int!) {
users(
limit: $limit
offset: $offset
order_by: { created_at: desc }
) {
id
username
email
created_at
}
users_aggregate {
aggregate {
count
}
}
}GraphQL Mutations
# 1. Create user
mutation CreateUser($email: String!, $username: String!, $fullName: String!) {
insert_users_one(object: {
email: $email
username: $username
full_name: $fullName
}) {
id
email
username
created_at
}
}
# 2. Update user
mutation UpdateUser($userId: uuid!, $fullName: String, $avatarUrl: String) {
update_users_by_pk(
pk_columns: { id: $userId }
_set: {
full_name: $fullName
avatar_url: $avatarUrl
updated_at: "now()"
}
) {
id
full_name
avatar_url
updated_at
}
}
# 3. Delete user
mutation DeleteUser($userId: uuid!) {
delete_users_by_pk(id: $userId) {
id
username
}
}
# 4. Bulk insert users
mutation BulkInsertUsers($users: [users_insert_input!]!) {
insert_users(objects: $users) {
affected_rows
returning {
id
username
email
}
}
}GraphQL Subscriptions
# 1. Watch all users (real-time updates)
subscription WatchUsers {
users(order_by: { created_at: desc }) {
id
username
email
created_at
}
}
# 2. Watch specific user changes
subscription WatchUserById($userId: uuid!) {
users_by_pk(id: $userId) {
id
username
full_name
avatar_url
updated_at
}
}
# 3. Watch new user registrations
subscription NewUserRegistrations {
users(
where: { created_at: { _gte: "now()" } }
order_by: { created_at: desc }
) {
id
username
email
created_at
}
}---
Example 2: Multi-Tenant SaaS Application
Database Schema
-- Organizations (tenants)
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
plan TEXT NOT NULL DEFAULT 'free',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Users belong to organizations
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Projects belong to organizations
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
owner_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Tasks belong to projects
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'todo',
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
assignee_id UUID REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_org ON users(organization_id);
CREATE INDEX idx_projects_org ON projects(organization_id);
CREATE INDEX idx_tasks_project ON tasks(project_id);Permissions Configuration
# users table permissions
- table:
name: users
schema: public
select_permissions:
- role: user
permission:
filter:
organization_id: { _eq: X-Hasura-Org-Id }
columns:
- id
- email
- username
- role
- created_at
insert_permissions:
- role: admin
permission:
check:
organization_id: { _eq: X-Hasura-Org-Id }
columns:
- email
- username
- organization_id
- role
update_permissions:
- role: admin
permission:
filter:
organization_id: { _eq: X-Hasura-Org-Id }
check:
organization_id: { _eq: X-Hasura-Org-Id }
columns:
- username
- role
# projects table permissions
- table:
name: projects
schema: public
select_permissions:
- role: user
permission:
filter:
organization_id: { _eq: X-Hasura-Org-Id }
columns: "*"
insert_permissions:
- role: user
permission:
check:
organization_id: { _eq: X-Hasura-Org-Id }
set:
organization_id: X-Hasura-Org-Id
owner_id: X-Hasura-User-Id
columns:
- name
- description
update_permissions:
- role: user
permission:
filter:
_and:
- organization_id: { _eq: X-Hasura-Org-Id }
- owner_id: { _eq: X-Hasura-User-Id }
columns:
- name
- description
# tasks table permissions
- table:
name: tasks
schema: public
select_permissions:
- role: user
permission:
filter:
project:
organization_id: { _eq: X-Hasura-Org-Id }
columns: "*"
insert_permissions:
- role: user
permission:
check:
project:
organization_id: { _eq: X-Hasura-Org-Id }
columns:
- title
- description
- status
- project_id
- assignee_id
update_permissions:
- role: user
permission:
filter:
project:
organization_id: { _eq: X-Hasura-Org-Id }
columns:
- title
- description
- status
- assignee_idJWT Configuration
{
"sub": "user-uuid",
"https://hasura.io/jwt/claims": {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": ["user", "admin"],
"x-hasura-user-id": "user-uuid",
"x-hasura-org-id": "org-uuid"
}
}GraphQL Queries
# Get organization with all projects and tasks
query GetOrganizationData {
organizations {
id
name
plan
projects(order_by: { created_at: desc }) {
id
name
description
owner {
id
username
}
tasks_aggregate {
aggregate {
count
}
}
tasks(
where: { status: { _eq: "todo" } }
limit: 5
) {
id
title
assignee {
username
}
}
}
}
}
# Get my tasks across all projects
query MyTasks {
tasks(
where: { assignee_id: { _eq: "X-Hasura-User-Id" } }
order_by: { created_at: desc }
) {
id
title
status
project {
name
organization {
name
}
}
}
}---
Example 3: Social Media Platform
Database Schema
-- Users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
bio TEXT,
avatar_url TEXT,
is_verified BOOLEAN DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Posts
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
image_url TEXT,
is_public BOOLEAN DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Follows (user follows another user)
CREATE TABLE follows (
follower_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
following_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (follower_id, following_id),
CHECK (follower_id != following_id)
);
-- Likes
CREATE TABLE likes (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, post_id)
);
-- Comments
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user ON posts(user_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_following ON follows(following_id);
CREATE INDEX idx_likes_post ON likes(post_id);
CREATE INDEX idx_comments_post ON comments(post_id);Permissions: Posts Table
# Posts - Users can see own posts, public posts, and posts from followed users
- table:
name: posts
schema: public
select_permissions:
- role: user
permission:
filter:
_or:
- user_id: { _eq: X-Hasura-User-Id }
- is_public: { _eq: true }
- user:
followers:
follower_id: { _eq: X-Hasura-User-Id }
columns:
- id
- user_id
- content
- image_url
- is_public
- created_at
- updated_at
insert_permissions:
- role: user
permission:
check: {}
set:
user_id: X-Hasura-User-Id
columns:
- content
- image_url
- is_public
update_permissions:
- role: user
permission:
filter:
user_id: { _eq: X-Hasura-User-Id }
check:
user_id: { _eq: X-Hasura-User-Id }
columns:
- content
- is_public
delete_permissions:
- role: user
permission:
filter:
user_id: { _eq: X-Hasura-User-Id }GraphQL Queries
# Get user profile with stats
query GetUserProfile($username: String!) {
users(where: { username: { _eq: $username } }) {
id
username
bio
avatar_url
is_verified
posts_aggregate {
aggregate {
count
}
}
followers_aggregate {
aggregate {
count
}
}
following_aggregate {
aggregate {
count
}
}
posts(
limit: 10
order_by: { created_at: desc }
) {
id
content
image_url
created_at
likes_aggregate {
aggregate {
count
}
}
comments_aggregate {
aggregate {
count
}
}
}
}
}
# Get feed (posts from followed users)
query GetFeed($limit: Int = 20, $offset: Int = 0) {
posts(
where: {
user: {
followers: {
follower_id: { _eq: "X-Hasura-User-Id" }
}
}
}
order_by: { created_at: desc }
limit: $limit
offset: $offset
) {
id
content
image_url
created_at
user {
id
username
avatar_url
is_verified
}
likes_aggregate {
aggregate {
count
}
}
likes(where: { user_id: { _eq: "X-Hasura-User-Id" } }) {
user_id
}
comments_aggregate {
aggregate {
count
}
}
comments(limit: 3, order_by: { created_at: desc }) {
id
content
user {
username
avatar_url
}
created_at
}
}
}
# Search users
query SearchUsers($searchTerm: String!) {
users(
where: {
_or: [
{ username: { _ilike: $searchTerm } }
{ bio: { _ilike: $searchTerm } }
]
}
limit: 20
) {
id
username
bio
avatar_url
is_verified
followers_aggregate {
aggregate {
count
}
}
}
}GraphQL Mutations
# Create post
mutation CreatePost($content: String!, $imageUrl: String, $isPublic: Boolean = true) {
insert_posts_one(object: {
content: $content
image_url: $imageUrl
is_public: $isPublic
}) {
id
content
image_url
created_at
}
}
# Like post
mutation LikePost($postId: uuid!) {
insert_likes_one(object: {
post_id: $postId
}) {
post_id
user_id
created_at
}
}
# Unlike post
mutation UnlikePost($postId: uuid!) {
delete_likes_by_pk(
post_id: $postId
user_id: "X-Hasura-User-Id"
) {
post_id
}
}
# Follow user
mutation FollowUser($followingId: uuid!) {
insert_follows_one(object: {
following_id: $followingId
}) {
follower_id
following_id
created_at
}
}
# Add comment
mutation AddComment($postId: uuid!, $content: String!) {
insert_comments_one(object: {
post_id: $postId
content: $content
}) {
id
content
created_at
user {
username
avatar_url
}
}
}Real-Time Subscriptions
# Watch post likes and comments in real-time
subscription WatchPost($postId: uuid!) {
posts_by_pk(id: $postId) {
id
content
likes_aggregate {
aggregate {
count
}
}
comments_aggregate {
aggregate {
count
}
}
comments(order_by: { created_at: desc }, limit: 10) {
id
content
created_at
user {
username
avatar_url
}
}
}
}
# Watch for new posts from followed users
subscription WatchFeed {
posts(
where: {
user: {
followers: {
follower_id: { _eq: "X-Hasura-User-Id" }
}
}
}
order_by: { created_at: desc }
limit: 20
) {
id
content
image_url
created_at
user {
username
avatar_url
}
}
}---
Example 4: E-Commerce Platform
Database Schema
-- Products
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
stock_quantity INT NOT NULL DEFAULT 0,
category TEXT NOT NULL,
image_url TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Orders
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
total DECIMAL(10,2) NOT NULL,
shipping_address TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Order items
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id),
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_order_items_order ON order_items(order_id);Event Trigger: Order Confirmation
# Event trigger configuration
event_triggers:
- name: order_created
table:
name: orders
schema: public
webhook: https://myapp.com/webhooks/order-created
insert:
columns: "*"
retry_conf:
num_retries: 3
interval_sec: 10
timeout_sec: 60Webhook Handler (Node.js):
const express = require('express');
const app = express();
app.post('/webhooks/order-created', async (req, res) => {
const { event } = req.body;
const order = event.data.new;
try {
// 1. Fetch full order details
const orderDetails = await fetchOrderDetails(order.id);
// 2. Send confirmation email
await sendEmail({
to: orderDetails.user.email,
subject: `Order Confirmation #${order.id}`,
template: 'order-confirmation',
data: {
orderId: order.id,
items: orderDetails.order_items,
total: order.total,
shippingAddress: order.shipping_address
}
});
// 3. Update inventory
for (const item of orderDetails.order_items) {
await updateInventory(item.product_id, -item.quantity);
}
// 4. Notify shipping service
await notifyShippingService({
orderId: order.id,
address: order.shipping_address,
items: orderDetails.order_items
});
res.json({ success: true });
} catch (error) {
console.error('Order webhook error:', error);
res.status(500).json({ error: error.message });
}
});
async function fetchOrderDetails(orderId) {
const query = `
query GetOrder($orderId: uuid!) {
orders_by_pk(id: $orderId) {
id
total
shipping_address
user {
email
username
}
order_items {
product {
name
}
quantity
price
}
}
}
`;
const response = await fetch('https://myhasura.app/v1/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({
query,
variables: { orderId }
})
});
const { data } = await response.json();
return data.orders_by_pk;
}GraphQL Mutations
# Create order (complex transaction)
mutation CreateOrder(
$shippingAddress: String!
$orderItems: [order_items_insert_input!]!
) {
insert_orders_one(object: {
shipping_address: $shippingAddress
total: 0 # Calculated in action
order_items: {
data: $orderItems
}
}) {
id
status
total
created_at
order_items {
product {
name
}
quantity
price
}
}
}---
Example 5: Real-Time Collaboration App
Database Schema
-- Documents
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content JSONB NOT NULL DEFAULT '{}'::jsonb,
owner_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Document collaborators
CREATE TABLE document_collaborators (
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permission TEXT NOT NULL CHECK (permission IN ('read', 'write', 'admin')),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (document_id, user_id)
);
-- Document versions (history)
CREATE TABLE document_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
content JSONB NOT NULL,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_docs_owner ON documents(owner_id);
CREATE INDEX idx_collaborators_doc ON document_collaborators(document_id);
CREATE INDEX idx_versions_doc ON document_versions(document_id);Permissions
# Documents - Access if owner or collaborator
- table:
name: documents
schema: public
select_permissions:
- role: user
permission:
filter:
_or:
- owner_id: { _eq: X-Hasura-User-Id }
- collaborators:
user_id: { _eq: X-Hasura-User-Id }
columns: "*"
update_permissions:
- role: user
permission:
filter:
_or:
- owner_id: { _eq: X-Hasura-User-Id }
- collaborators:
_and:
- user_id: { _eq: X-Hasura-User-Id }
- permission: { _in: ["write", "admin"] }
columns:
- title
- content
set:
updated_at: now()Real-Time Collaboration Subscription
# Subscribe to document changes
subscription WatchDocument($documentId: uuid!) {
documents_by_pk(id: $documentId) {
id
title
content
updated_at
owner {
id
username
avatar_url
}
collaborators {
user {
id
username
avatar_url
}
permission
}
}
}
# Watch all collaborators' cursors/selections (using presence)
subscription WatchCollaborators($documentId: uuid!) {
document_collaborators(
where: { document_id: { _eq: $documentId } }
) {
user {
id
username
avatar_url
}
permission
}
}Optimistic UI Updates
// React example with Apollo Client
const [updateDocument] = useMutation(UPDATE_DOCUMENT, {
optimisticResponse: {
update_documents_by_pk: {
__typename: 'documents',
id: documentId,
title: newTitle,
content: newContent,
updated_at: new Date().toISOString()
}
}
});
// Update with optimistic UI
await updateDocument({
variables: {
documentId,
title: newTitle,
content: newContent
}
});---
Example 6: User Authentication System
Action: User Login
GraphQL SDL:
type Mutation {
login(username: String!, password: String!): LoginResponse
}
type LoginResponse {
accessToken: String!
refreshToken: String!
user: User!
}
type User {
id: uuid!
username: String!
email: String!
}Action Handler (Express):
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
app.post('/actions/login', async (req, res) => {
const { input } = req.body;
const { username, password } = input;
try {
// 1. Fetch user from database
const userQuery = `
query GetUser($username: String!) {
users(where: { username: { _eq: $username } }, limit: 1) {
id
email
username
password_hash
}
}
`;
const userResponse = await fetch(process.env.HASURA_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({
query: userQuery,
variables: { username }
})
});
const { data } = await userResponse.json();
const user = data.users[0];
if (!user) {
return res.status(401).json({
message: 'Invalid credentials'
});
}
// 2. Verify password
const validPassword = await bcrypt.compare(password, user.password_hash);
if (!validPassword) {
return res.status(401).json({
message: 'Invalid credentials'
});
}
// 3. Generate JWT tokens
const accessToken = jwt.sign(
{
sub: user.id,
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.id
}
},
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// 4. Return response
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
username: user.username,
email: user.email
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
message: 'Internal server error'
});
}
});Action: User Signup
type Mutation {
signup(
email: String!
username: String!
password: String!
): SignupResponse
}
type SignupResponse {
accessToken: String!
user: User!
}Handler:
app.post('/actions/signup', async (req, res) => {
const { input } = req.body;
const { email, username, password } = input;
try {
// 1. Validate input
if (password.length < 8) {
return res.status(400).json({
message: 'Password must be at least 8 characters'
});
}
// 2. Hash password
const passwordHash = await bcrypt.hash(password, 10);
// 3. Create user
const createUserMutation = `
mutation CreateUser(
$email: String!
$username: String!
$passwordHash: String!
) {
insert_users_one(object: {
email: $email
username: $username
password_hash: $passwordHash
}) {
id
email
username
}
}
`;
const response = await fetch(process.env.HASURA_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({
query: createUserMutation,
variables: { email, username, passwordHash }
})
});
const { data, errors } = await response.json();
if (errors) {
if (errors[0].message.includes('Uniqueness violation')) {
return res.status(400).json({
message: 'Email or username already exists'
});
}
throw new Error(errors[0].message);
}
const user = data.insert_users_one;
// 4. Generate JWT
const accessToken = jwt.sign(
{
sub: user.id,
'https://hasura.io/jwt/claims': {
'x-hasura-default-role': 'user',
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.id
}
},
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({
accessToken,
user: {
id: user.id,
username: user.username,
email: user.email
}
});
} catch (error) {
console.error('Signup error:', error);
res.status(500).json({
message: 'Internal server error'
});
}
});---
Example 7: Event-Driven Order Processing
Event Trigger: Order Status Changes
event_triggers:
- name: order_status_changed
table:
name: orders
schema: public
webhook: https://myapp.com/webhooks/order-status
update:
columns: [status]
retry_conf:
num_retries: 5
interval_sec: 10Webhook Handler:
app.post('/webhooks/order-status', async (req, res) => {
const { event } = req.body;
const oldStatus = event.data.old.status;
const newStatus = event.data.new.status;
const order = event.data.new;
try {
// Handle different status transitions
switch (newStatus) {
case 'paid':
await handleOrderPaid(order);
break;
case 'shipped':
await handleOrderShipped(order);
break;
case 'delivered':
await handleOrderDelivered(order);
break;
case 'canceled':
await handleOrderCanceled(order);
break;
}
res.json({ success: true });
} catch (error) {
console.error('Order status webhook error:', error);
res.status(500).json({ error: error.message });
}
});
async function handleOrderPaid(order) {
// 1. Send payment confirmation email
await sendEmail({
to: order.user_email,
template: 'payment-confirmed',
data: { orderId: order.id, total: order.total }
});
// 2. Notify warehouse to prepare shipment
await notifyWarehouse({
orderId: order.id,
items: await getOrderItems(order.id)
});
// 3. Update analytics
await trackEvent('order_paid', {
orderId: order.id,
total: order.total,
userId: order.user_id
});
}
async function handleOrderShipped(order) {
// 1. Send shipping notification
await sendEmail({
to: order.user_email,
template: 'order-shipped',
data: {
orderId: order.id,
trackingNumber: order.tracking_number
}
});
// 2. Send SMS notification (if enabled)
if (order.notify_via_sms) {
await sendSMS({
to: order.phone,
message: `Your order #${order.id} has shipped! Track: ${order.tracking_url}`
});
}
}
async function handleOrderDelivered(order) {
// 1. Send delivery confirmation
await sendEmail({
to: order.user_email,
template: 'order-delivered',
data: { orderId: order.id }
});
// 2. Request review after 3 days
await scheduleTask({
task: 'request_review',
delay: '3 days',
data: { orderId: order.id }
});
}
async function handleOrderCanceled(order) {
// 1. Process refund
await processRefund({
orderId: order.id,
amount: order.total,
reason: order.cancel_reason
});
// 2. Restore inventory
const items = await getOrderItems(order.id);
for (const item of items) {
await updateInventory(item.product_id, item.quantity);
}
// 3. Send cancellation email
await sendEmail({
to: order.user_email,
template: 'order-canceled',
data: { orderId: order.id, refundAmount: order.total }
});
}---
Example 8: Custom Payment Action
Action Definition
type Mutation {
processPayment(
orderId: ID!
paymentMethodId: String!
amount: Float!
currency: String!
): PaymentResponse
}
type PaymentResponse {
success: Boolean!
transactionId: String
orderId: ID!
error: String
}Handler (Stripe Integration):
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/actions/process-payment', async (req, res) => {
const { input, session_variables } = req.body;
const { orderId, paymentMethodId, amount, currency } = input;
const userId = session_variables['x-hasura-user-id'];
try {
// 1. Verify order belongs to user
const orderQuery = `
query GetOrder($orderId: uuid!, $userId: uuid!) {
orders_by_pk(id: $orderId) {
id
user_id
total
status
}
}
`;
const orderResponse = await hasuraRequest(orderQuery, {
orderId,
userId
});
const order = orderResponse.data.orders_by_pk;
if (!order) {
return res.status(404).json({
message: 'Order not found'
});
}
if (order.user_id !== userId) {
return res.status(403).json({
message: 'Unauthorized'
});
}
if (order.status !== 'pending') {
return res.status(400).json({
message: 'Order already processed'
});
}
// 2. Create Stripe payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Convert to cents
currency: currency,
payment_method: paymentMethodId,
confirm: true,
metadata: {
orderId: orderId,
userId: userId
}
});
// 3. Update order status
if (paymentIntent.status === 'succeeded') {
const updateMutation = `
mutation UpdateOrder($orderId: uuid!) {
update_orders_by_pk(
pk_columns: { id: $orderId }
_set: { status: "paid" }
) {
id
status
}
}
`;
await hasuraRequest(updateMutation, { orderId });
return res.json({
success: true,
transactionId: paymentIntent.id,
orderId: orderId,
error: null
});
} else {
return res.json({
success: false,
transactionId: null,
orderId: orderId,
error: 'Payment failed'
});
}
} catch (error) {
console.error('Payment processing error:', error);
return res.json({
success: false,
transactionId: null,
orderId: orderId,
error: error.message
});
}
});
async function hasuraRequest(query, variables) {
const response = await fetch(process.env.HASURA_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({ query, variables })
});
return await response.json();
}---
Example 9: Remote Schema Integration
Add Auth0 Management API as Remote Schema
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/api/v2/",
"headers": [
{
"name": "Authorization",
"value": "Bearer ${AUTH0_MANAGEMENT_TOKEN}"
}
],
"forward_client_headers": false,
"timeout_seconds": 60
}
}
}Remote Schema Permissions
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "add_remote_schema_permissions",
"args": {
"remote_schema": "auth0_api",
"role": "user",
"definition": {
"schema": "type User { id: ID! email: String! name: String } type Query { user(id: ID! @preset(value: \"x-hasura-user-id\")): User }"
}
}
}Query Local + Remote Data
query GetUserWithAuth0Profile {
users_by_pk(id: "user-uuid") {
id
username
created_at
# Remote schema field
auth0_profile {
email
email_verified
last_login
logins_count
}
}
}---
Example 10: Advanced Permissions Patterns
Time-Based Permissions
# Only allow updates during business hours
update_permissions:
- role: user
permission:
filter:
_and:
- user_id: { _eq: X-Hasura-User-Id }
- created_at: { _gte: "now() - interval '24 hours'" }Hierarchical Permissions
-- Organization hierarchy
CREATE TABLE org_hierarchy (
parent_id UUID REFERENCES organizations(id),
child_id UUID REFERENCES organizations(id),
PRIMARY KEY (parent_id, child_id)
);# Access data from own org and child orgs
select_permissions:
- role: manager
permission:
filter:
_or:
- organization_id: { _eq: X-Hasura-Org-Id }
- organization:
parent_orgs:
parent_id: { _eq: X-Hasura-Org-Id }Computed Field Permissions
-- Function to check if user can edit post
CREATE FUNCTION can_edit_post(post_row posts, hasura_session json)
RETURNS boolean AS $$
SELECT
post_row.user_id = (hasura_session->>'x-hasura-user-id')::uuid
OR
(hasura_session->>'x-hasura-role') = 'admin'
$$ LANGUAGE sql STABLE;# Use computed field in permissions
update_permissions:
- role: user
permission:
filter:
can_edit_post:
_eq: true---
Example 11: Real-Time Analytics Dashboard
SQL Functions for Analytics
-- Daily active users
CREATE FUNCTION daily_active_users(date_param date)
RETURNS TABLE (date date, count bigint) AS $$
SELECT
date_param as date,
COUNT(DISTINCT user_id) as count
FROM user_activities
WHERE DATE(created_at) = date_param
$$ LANGUAGE sql STABLE;
-- Revenue by day
CREATE FUNCTION revenue_by_day(start_date date, end_date date)
RETURNS TABLE (date date, revenue decimal) AS $$
SELECT
DATE(created_at) as date,
SUM(total) as revenue
FROM orders
WHERE
status = 'completed'
AND DATE(created_at) BETWEEN start_date AND end_date
GROUP BY DATE(created_at)
ORDER BY date
$$ LANGUAGE sql STABLE;Track Functions in Hasura
functions:
- function:
name: daily_active_users
schema: public
- function:
name: revenue_by_day
schema: publicReal-Time Dashboard Subscription
subscription DashboardMetrics {
# Real-time order count
orders_aggregate(
where: { created_at: { _gte: "today" } }
) {
aggregate {
count
sum {
total
}
}
}
# Real-time user signups
users_aggregate(
where: { created_at: { _gte: "today" } }
) {
aggregate {
count
}
}
# Top products today
order_items_aggregate(
where: { created_at: { _gte: "today" } }
group_by: [product_id]
order_by: { aggregate: { sum: { quantity: desc } } }
limit: 5
) {
aggregate {
sum {
quantity
}
}
nodes {
product {
name
}
}
}
}---
Example 12: File Upload with Actions
Action: Upload File
type Mutation {
uploadFile(
file: String! # Base64 encoded
fileName: String!
mimeType: String!
): FileUploadResponse
}
type FileUploadResponse {
url: String!
fileId: ID!
fileName: String!
}Handler (S3 Upload):
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const s3 = new AWS.S3({
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY
});
app.post('/actions/upload-file', async (req, res) => {
const { input, session_variables } = req.body;
const { file, fileName, mimeType } = input;
const userId = session_variables['x-hasura-user-id'];
try {
// 1. Decode base64 file
const fileBuffer = Buffer.from(file, 'base64');
// 2. Generate unique file ID
const fileId = uuidv4();
const fileExtension = fileName.split('.').pop();
const s3Key = `uploads/${userId}/${fileId}.${fileExtension}`;
// 3. Upload to S3
const uploadParams = {
Bucket: process.env.S3_BUCKET,
Key: s3Key,
Body: fileBuffer,
ContentType: mimeType,
ACL: 'public-read'
};
const uploadResult = await s3.upload(uploadParams).promise();
// 4. Save file metadata to database
const saveFileMutation = `
mutation SaveFile(
$fileId: uuid!
$userId: uuid!
$fileName: String!
$mimeType: String!
$url: String!
$size: Int!
) {
insert_files_one(object: {
id: $fileId
user_id: $userId
file_name: $fileName
mime_type: $mimeType
url: $url
size: $size
}) {
id
}
}
`;
await hasuraRequest(saveFileMutation, {
fileId,
userId,
fileName,
mimeType,
url: uploadResult.Location,
size: fileBuffer.length
});
// 5. Return response
res.json({
url: uploadResult.Location,
fileId: fileId,
fileName: fileName
});
} catch (error) {
console.error('File upload error:', error);
res.status(500).json({
message: 'File upload failed',
error: error.message
});
}
});---
Example 13: GraphQL Query Optimization
Using Query Caching
# Add @cached directive
query GetProducts @cached(ttl: 300) {
products(
where: { is_active: { _eq: true } }
order_by: { created_at: desc }
limit: 20
) {
id
name
price
image_url
}
}Pagination with Cursors
# Cursor-based pagination (more efficient than offset)
query GetPostsPaginated($cursor: timestamptz, $limit: Int = 20) {
posts(
where: { created_at: { _lt: $cursor } }
order_by: { created_at: desc }
limit: $limit
) {
id
title
content
created_at
}
}
# Next page: use last post's created_at as cursorEfficient Aggregations
# Get counts without fetching all data
query GetStats {
users_aggregate {
aggregate {
count
}
}
posts_aggregate(where: { created_at: { _gte: "2025-01-01" } }) {
aggregate {
count
}
}
# Group by and aggregate
posts_aggregate(
group_by: [user_id]
order_by: { aggregate: { count: desc } }
limit: 10
) {
aggregate {
count
}
nodes {
user {
username
}
}
}
}---
Example 14: Automated Email Notifications
Event Trigger: New Comment Notification
event_triggers:
- name: comment_added
table:
name: comments
schema: public
webhook: https://myapp.com/webhooks/comment-added
insert:
columns: "*"Webhook Handler:
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
app.post('/webhooks/comment-added', async (req, res) => {
const { event } = req.body;
const comment = event.data.new;
try {
// 1. Fetch comment with post and user details
const query = `
query GetCommentDetails($commentId: uuid!) {
comments_by_pk(id: $commentId) {
id
content
user {
username
}
post {
id
title
user {
id
email
username
}
}
}
}
`;
const response = await hasuraRequest(query, {
commentId: comment.id
});
const commentData = response.data.comments_by_pk;
const postAuthor = commentData.post.user;
// 2. Don't notify if commenting on own post
if (commentData.user.id === postAuthor.id) {
return res.json({ success: true, skipped: true });
}
// 3. Send email notification
const msg = {
to: postAuthor.email,
from: 'notifications@myapp.com',
subject: `New comment on "${commentData.post.title}"`,
html: `
<h2>New Comment</h2>
<p><strong>${commentData.user.username}</strong> commented on your post:</p>
<blockquote>${commentData.content}</blockquote>
<p><a href="https://myapp.com/posts/${commentData.post.id}">View Post</a></p>
`
};
await sgMail.send(msg);
res.json({ success: true });
} catch (error) {
console.error('Comment notification error:', error);
res.status(500).json({ error: error.message });
}
});---
Example 15: Admin Panel with Row-Level Security
Schema
-- Users with different roles
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'user',
CHECK (role IN ('user', 'moderator', 'admin'))
);
-- Content that can be moderated
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
content TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'published',
CHECK (status IN ('draft', 'published', 'flagged', 'removed'))
);Role-Based Permissions
# Regular users - can only manage own posts
- table:
name: posts
schema: public
select_permissions:
- role: user
permission:
filter:
user_id: { _eq: X-Hasura-User-Id }
columns: "*"
update_permissions:
- role: user
permission:
filter:
user_id: { _eq: X-Hasura-User-Id }
columns: [content, status]
check:
status: { _in: ["draft", "published"] }
# Moderators - can view all, flag inappropriate content
- table:
name: posts
schema: public
select_permissions:
- role: moderator
permission:
filter: {} # Can see all posts
columns: "*"
update_permissions:
- role: moderator
permission:
filter: {}
columns: [status]
check:
status: { _in: ["flagged", "removed"] }
# Admins - full access
- table:
name: posts
schema: public
select_permissions:
- role: admin
permission:
filter: {}
columns: "*"
insert_permissions:
- role: admin
permission:
check: {}
columns: "*"
update_permissions:
- role: admin
permission:
filter: {}
columns: "*"
delete_permissions:
- role: admin
permission:
filter: {}---
Example 16: API Gateway Pattern
Unified GraphQL API from Multiple Sources
Hasura as API Gateway:
1. Local PostgreSQL database (users, orders) 2. Remote GraphQL API (payment service) 3. REST API via Actions (shipping service)
Remote Schema: Payments API
POST /v1/metadata HTTP/1.1
{
"type": "add_remote_schema",
"args": {
"name": "payments",
"definition": {
"url": "https://payments.myapp.com/graphql"
}
}
}Action: Get Shipping Status (REST to GraphQL)
type Query {
getShippingStatus(trackingNumber: String!): ShippingStatus
}
type ShippingStatus {
trackingNumber: String!
status: String!
estimatedDelivery: String
location: String
}Unified Query:
query GetOrderDetails($orderId: uuid!) {
# Local database
orders_by_pk(id: $orderId) {
id
total
created_at
# Local relationship
user {
email
username
}
# Remote schema relationship
payment {
transactionId
status
amount
}
# Action (REST API)
shippingStatus(trackingNumber: $trackingNumber) {
status
estimatedDelivery
location
}
}
}---
Example 17: Metadata API Automation
Automate Hasura Configuration
Script to setup permissions for all tables:
const fetch = require('node-fetch');
const HASURA_ENDPOINT = process.env.HASURA_ENDPOINT;
const ADMIN_SECRET = process.env.HASURA_ADMIN_SECRET;
async function hasuraMetadataRequest(type, args) {
const response = await fetch(`${HASURA_ENDPOINT}/v1/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': ADMIN_SECRET
},
body: JSON.stringify({ type, args })
});
return await response.json();
}
async function setupTablePermissions(tableName) {
// Select permission for 'user' role
await hasuraMetadataRequest('pg_create_select_permission', {
table: { name: tableName, schema: 'public' },
role: 'user',
permission: {
filter: {
user_id: { _eq: 'X-Hasura-User-Id' }
},
columns: '*'
}
});
// Insert permission
await hasuraMetadataRequest('pg_create_insert_permission', {
table: { name: tableName, schema: 'public' },
role: 'user',
permission: {
check: {},
set: {
user_id: 'X-Hasura-User-Id'
},
columns: '*'
}
});
console.log(`Permissions set for ${tableName}`);
}
async function main() {
const tables = ['posts', 'comments', 'likes'];
for (const table of tables) {
await setupTablePermissions(table);
}
console.log('All permissions configured!');
}
main();---
Summary
These 17 examples cover:
1. Basic CRUD operations 2. Multi-tenant SaaS architecture 3. Social media with complex permissions 4. E-commerce with event triggers 5. Real-time collaboration 6. Authentication actions 7. Event-driven order processing 8. Payment processing 9. Remote schema integration 10. Advanced permission patterns 11. Real-time analytics 12. File uploads 13. Query optimization 14. Automated notifications 15. Admin panels with RLS 16. API gateway pattern 17. Metadata automation
Each example demonstrates production-ready patterns you can adapt for your applications.
---
Version: 1.0.0 Last Updated: January 2025
Hasura GraphQL Engine Skill
Comprehensive skill for building production-ready GraphQL APIs with Hasura GraphQL Engine. Master instant API generation, granular permissions, authentication integration, event-driven architectures, and custom business logic.
Overview
Hasura GraphQL Engine is an instant GraphQL API generator that provides:
- Instant APIs: Auto-generate GraphQL APIs from PostgreSQL databases
- Real-time: Built-in GraphQL subscriptions for live data
- Permissions: Granular row-level and column-level security
- Authentication: JWT and webhook-based auth integration
- Event Triggers: Database change webhooks for event-driven architectures
- Actions: Extend GraphQL with custom business logic
- Remote Schemas: Stitch multiple GraphQL services together
- Production Ready: Caching, rate limiting, monitoring out of the box
The Hasura Value Proposition
Traditional GraphQL Backend
// Traditional approach: Write resolvers manually
const resolvers = {
Query: {
users: async (parent, args, context) => {
// Auth check
if (!context.user) throw new Error('Unauthorized');
// Build query
let query = db.select('*').from('users');
// Apply filters
if (args.where) {
query = query.where(args.where);
}
// Apply pagination
if (args.limit) {
query = query.limit(args.limit);
}
// Execute
return await query;
}
},
Mutation: {
insertUser: async (parent, args, context) => {
// Auth check
if (!context.user) throw new Error('Unauthorized');
// Validation
if (!args.email) throw new Error('Email required');
// Insert
return await db('users').insert(args).returning('*');
}
}
};
// 100+ lines of code for basic CRUD
// Manual permission handling
// No real-time subscriptions
// Custom caching logic neededHasura Approach
# Point to database, get instant API
HASURA_GRAPHQL_DATABASE_URL=postgres://...
# Define permissions once
tables:
- table:
name: users
schema: public
select_permissions:
- role: user
permission:
filter:
id: { _eq: X-Hasura-User-Id }
columns: [id, email, username]
# GraphQL API ready with:
# ✓ Queries, mutations, subscriptions
# ✓ Filtering, sorting, pagination
# ✓ Relationships and nested queries
# ✓ Real-time updates
# ✓ Row-level security
# ✓ Column-level security
# ✓ Zero custom codeResult: 10x faster API development with enterprise-grade security.
Quick Start
1. Run Hasura with Docker
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: postgrespassword
volumes:
- db_data:/var/lib/postgresql/data
hasura:
image: hasura/graphql-engine:v2.36.0
ports:
- "8080:8080"
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
volumes:
db_data:docker-compose up -d2. Access Hasura Console
Open http://localhost:8080/console
Admin secret: myadminsecretkey
3. Create Your First Table
SQL:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);Track in Hasura: Data tab → Track table
4. Query Your API
# Query
query GetUsers {
users {
id
email
username
created_at
}
}
# Insert
mutation CreateUser {
insert_users_one(object: {
email: "user@example.com"
username: "johndoe"
}) {
id
email
username
}
}
# Subscribe (real-time)
subscription WatchUsers {
users {
id
username
created_at
}
}That's it! Fully functional GraphQL API in minutes.
Core Capabilities
Instant GraphQL API
Track a PostgreSQL table and immediately get:
- Queries:
users,users_by_pk,users_aggregate - Mutations:
insert_users,update_users,delete_users - Subscriptions: Real-time updates for all queries
- Filtering:
whereclauses with operators (_eq,_gt,_like,_in, etc.) - Sorting:
order_byon any column - Pagination:
limitandoffset - Relationships: Auto-detected from foreign keys
- Aggregations:
count,sum,avg,max,min
Granular Permissions
Define who can access what data with precision:
# Example: Users can only see and update their own profile
select_permissions:
- role: user
permission:
filter:
id: { _eq: X-Hasura-User-Id }
columns: [id, email, username, avatar_url]
update_permissions:
- role: user
permission:
filter:
id: { _eq: X-Hasura-User-Id }
columns: [username, avatar_url]
set:
updated_at: now()Permission Features:
- Row-level security with boolean expressions
- Column-level security (hide sensitive fields)
- Session variables from JWT/webhook
- Validation with
checkconstraints - Auto-set columns (e.g.,
user_id,updated_at)
Authentication Integration
Hasura validates and authorizes, you handle authentication:
JWT Mode:
{
"sub": "user123",
"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"
}
}Webhook Mode:
// Your auth webhook
app.post('/auth', (req, res) => {
const token = req.headers['authorization'];
const user = validateToken(token);
res.json({
'X-Hasura-User-Id': user.id,
'X-Hasura-Role': user.role
});
});Supported auth providers:
- Auth0
- Firebase Authentication
- AWS Cognito
- Supabase Auth
- Custom JWT issuer
- Any webhook
Event Triggers
Turn database changes into events:
# Send welcome email when user signs up
event_triggers:
- name: user_created
table:
name: users
schema: public
webhook: https://myapp.com/webhooks/user-created
insert:
columns: "*"Webhook receives:
{
"event": {
"op": "INSERT",
"data": {
"old": null,
"new": {
"id": "uuid",
"email": "user@example.com"
}
}
}
}Use cases:
- Send emails/SMS
- Sync to Elasticsearch
- Update cache
- Trigger workflows
- External integrations
Actions (Custom Logic)
Extend GraphQL with custom business logic:
# Define custom mutation
type Mutation {
login(username: String!, password: String!): LoginResponse
}
type LoginResponse {
accessToken: String!
user: User!
}Handler (your code):
app.post('/actions/login', async (req, res) => {
const { username, password } = req.body.input;
const user = await validateCredentials(username, password);
const token = generateJWT(user);
res.json({
accessToken: token,
user: user
});
});Use cases:
- Login/signup
- Payment processing
- Complex validations
- Third-party API calls
- File uploads
Remote Schemas
Stitch multiple GraphQL APIs:
# Add external GraphQL API
remote_schemas:
- name: countries
definition:
url: https://countries.trevorblades.com/graphqlQuery multiple sources:
query {
# Local database
users {
id
username
country_code
# Remote schema
country {
name
emoji
capital
}
}
}Use cases:
- Microservices federation
- Third-party GraphQL APIs
- Legacy GraphQL services
- Multi-cloud architectures
Real-Time Subscriptions
Every query becomes a subscription:
# Live query - updates when data changes
subscription LiveOrders {
orders(
where: { status: { _eq: "pending" } }
order_by: { created_at: desc }
) {
id
total
status
items {
product {
name
}
quantity
}
}
}Features:
- WebSocket-based
- Multiplexed (efficient for many clients)
- Automatic change detection
- Filtering and sorting maintained
- Cursor-based pagination support
Architecture Overview
How Hasura Works
┌─────────────┐
│ Client │
│ (Web/App) │
└──────┬──────┘
│ GraphQL Query + JWT
│
▼
┌─────────────────────────────────────────┐
│ Hasura GraphQL Engine │
│ │
│ ┌───────────┐ ┌──────────────────┐ │
│ │ Auth │ │ Permission │ │
│ │ Validator │─▶│ Engine │ │
│ └───────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ GraphQL → SQL Compiler │ │
│ └──────────────────────────────────┘ │
└─────────────────┬───────────────────────┘
│ SQL Query
▼
┌─────────────────┐
│ PostgreSQL │
│ Database │
└─────────────────┘Request Flow:
1. Client sends GraphQL query with auth header 2. Hasura validates JWT/webhook 3. Extracts session variables (user_id, role, etc.) 4. Checks permissions for the role 5. Compiles GraphQL to optimized SQL 6. Applies row/column filters from permissions 7. Executes SQL on PostgreSQL 8. Returns GraphQL response
Metadata-Driven Design
Hasura configuration is metadata, not code:
hasura/
├── metadata/
│ ├── databases/
│ │ └── default/
│ │ ├── tables/
│ │ │ ├── public_users.yaml # Table config
│ │ │ └── public_posts.yaml
│ ├── actions.yaml # Custom mutations
│ ├── remote_schemas.yaml # External APIs
│ └── version.yaml
└── migrations/
└── default/
├── 1_create_users.up.sql
└── 2_create_posts.up.sqlBenefits:
- Version control your entire API
- Easy collaboration (Git-based)
- Environment promotion (dev → staging → prod)
- Declarative infrastructure-as-code
When to Use Hasura
Excellent For
✓ Rapid API Development: Need GraphQL API yesterday ✓ CRUD-Heavy Apps: Admin panels, dashboards, internal tools ✓ Real-Time Apps: Chat, collaboration, live dashboards ✓ Multi-Tenant SaaS: Built-in row-level security ✓ Microservices: Schema stitching for service federation ✓ Event-Driven: Database triggers to webhooks ✓ Prototyping: Validate ideas quickly ✓ Postgres-Centric: Your data is in PostgreSQL
Consider Alternatives If
⚠ Complex Business Logic: Heavy custom logic better in code ⚠ Non-Postgres Primary DB: Hasura is PostgreSQL-first ⚠ Graph Algorithms: Complex graph traversals not optimal ⚠ Batch Processing: Not designed for ETL workloads ⚠ File Storage Primary: Better solutions for file-heavy apps
Production Deployment
Key Production Settings
# Security
HASURA_GRAPHQL_ADMIN_SECRET=strong-random-secret
HASURA_GRAPHQL_JWT_SECRET='{"type":"RS256","key":"..."}'
# Performance
HASURA_GRAPHQL_ENABLE_CONSOLE=false
HASURA_GRAPHQL_DEV_MODE=false
# Rate Limiting
HASURA_GRAPHQL_RATE_LIMIT_PER_MINUTE=1000
# Connections
HASURA_GRAPHQL_PG_CONNECTIONS=50
# CORS
HASURA_GRAPHQL_CORS_DOMAIN=https://myapp.com
# Logging
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-logDeployment Options
Hasura Cloud (Managed):
- Global CDN
- Auto-scaling
- Monitoring/alerting
- Click-to-deploy
- Free tier available
Self-Hosted:
- Docker/Docker Compose
- Kubernetes (Helm charts available)
- AWS ECS/Fargate
- Google Cloud Run
- Azure Container Instances
Monitoring
- Health endpoint:
/healthz - Metrics: Prometheus integration
- Logging: Structured JSON logs
- APM: DataDog, New Relic integration
- Cloud dashboard: Built-in monitoring (Hasura Cloud)
Common Use Cases
1. SaaS Application Backend
Multi-tenant data isolation with row-level security:
# Automatic tenant isolation
select_permissions:
- role: user
permission:
filter:
organization_id: { _eq: X-Hasura-Org-Id }2. Real-Time Dashboards
Live data updates with subscriptions:
subscription LiveMetrics {
metrics_aggregate(
where: { created_at: { _gte: "2025-01-15" } }
) {
aggregate {
count
sum { value }
avg { value }
}
}
}3. E-Commerce Platform
Event triggers for order processing:
// Order created → Process payment
// Order paid → Update inventory
// Order shipped → Send notification4. Social Media App
Complex permissions for privacy:
# 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 }5. Internal Admin Tools
Rapid CRUD interface generation:
- Auto-generated queries/mutations
- Role-based access control
- Relationship traversal
- Bulk operations
Learning Path
Beginner (Week 1)
1. Run Hasura locally with Docker 2. Create tables and track them 3. Explore auto-generated GraphQL API 4. Set up basic select permissions 5. Integrate JWT authentication
Intermediate (Week 2-3)
1. Design permission system for your use case 2. Implement event triggers 3. Create custom actions 4. Set up migrations and metadata workflow 5. Deploy to staging environment
Advanced (Week 4+)
1. Add remote schemas 2. Optimize query performance 3. Implement caching strategy 4. Set up monitoring and alerting 5. Production deployment and CI/CD
Resources
Official
- Docs: https://hasura.io/docs
- Learn: https://hasura.io/learn (interactive tutorials)
- Cloud: https://cloud.hasura.io
- CLI: https://hasura.io/docs/latest/hasura-cli
Community
- Discord: https://discord.gg/hasura
- GitHub: https://github.com/hasura/graphql-engine
- Forum: https://github.com/hasura/graphql-engine/discussions
- Blog: https://hasura.io/blog
Tools
- Hasura Cloud Console: Managed platform
- Hasura CLI: Local development and migrations
- GraphQL Code Generator: Client code generation
- Apollo Client: Frontend integration
Next Steps
1. Read SKILL.md: Comprehensive reference guide 2. Review EXAMPLES.md: 15+ practical code examples 3. Run Quick Start: Get Hasura running locally 4. Build Sample App: Create todo app or blog 5. Deploy: Try Hasura Cloud free tier
Support
- Open issues on GitHub
- Ask questions on Discord
- Explore documentation
- Check community discussions
---
Version: 1.0.0 Updated: January 2025 License: Open Source (Apache 2.0)