hasura-graphql-engine by manutej/luxor-claude-marketplace
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill hasura-graphql-engine一项用于通过 Hasura 构建生产就绪的 GraphQL API 的综合技能。掌握即时 API 生成、细粒度权限、身份验证集成、事件驱动架构、自定义业务逻辑以及适用于现代应用程序的远程模式拼接。
在以下情况下使用 Hasura GraphQL 引擎:
Hasura 的主要价值主张是从数据库模式自动生成 GraphQL API:
工作原理:
query - 使用过滤、排序、分页获取数据广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
mutation - 插入、更新、删除操作subscription - 通过 WebSocket 实现实时数据更新Hasura 是元数据驱动的,而非代码驱动:
关键元数据组件:
Hasura 的权限系统是其最强大的功能,支持细粒度访问控制:
权限类型:
select - 读取权限insert - 创建权限update - 修改权限delete - 删除权限Hasura 将身份验证委托给您的身份验证服务,但处理授权:
x-hasura-role、x-hasura-user-id、自定义声明身份验证流程:
事件触发器通过在数据库更改时调用 Webhook 来启用事件驱动架构:
Actions 通过自定义业务逻辑扩展 Hasura:
常见用例:
远程模式通过合并外部 GraphQL API 实现模式拼接:
Hasura 提供原生 GraphQL 订阅:
行级安全性使用布尔检查表达式来过滤可访问的行:
示例:用户只能看到自己的数据
{
"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 被隐藏
# created_at 被隐藏
示例:不同角色的不同视图
# 管理员角色查看所有列
select:
columns: "*"
# 用户角色查看有限列
select:
columns:
- id
- username
- profile_picture
控制可以插入哪些数据:
示例:从会话设置 user_id
{
"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:可以选择哪些行进行更新 check:更新完成后进行验证 set:自动设置列值
控制哪些行可以被删除:
示例:仅删除自己的数据
{
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
配置 Hasura 以验证 JWT 令牌:
环境变量:
HASURA_GRAPHQL_JWT_SECRET='{
"type": "RS256",
"key": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}'
JWT 声明结构:
{
"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-role:如果请求中未指定,则为默认角色x-hasura-allowed-roles:用户可以承担的角色数组x-hasura-user-id添加 Hasura 声明的 Auth0 规则:
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);
}
客户端使用:
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 自定义声明:
// 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
}
});
}
JWT 的替代方案 - Hasura 为每个请求调用您的 Webhook:
Webhook 端点:
app.post('/auth-webhook', async (req, res) => {
const authHeader = req.headers['authorization'];
// 验证令牌(您的逻辑)
const user = await validateToken(authHeader);
if (!user) {
return res.status(401).json({ message: 'Unauthorized' });
}
// 返回会话变量
res.json({
'X-Hasura-User-Id': user.id,
'X-Hasura-Role': user.role,
'X-Hasura-Org-Id': user.orgId
});
});
Hasura 配置:
HASURA_GRAPHQL_AUTH_HOOK=https://myapp.com/auth-webhook
HASURA_GRAPHQL_AUTH_HOOK_MODE=POST
事件触发器在数据库更改时调用 Webhook:
通过控制台:
通过元数据 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
}
}
}
Webhook 接收结构化的 JSON 负载:
{
"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 处理程序
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 });
});
同步到 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 });
});
触发工作流:
app.post('/webhooks/order-placed', async (req, res) => {
const { event } = req.body;
const order = event.data.new;
// 触发支付处理
await processPayment(order.id);
// 通知库存系统
await updateInventory(order.items);
// 发送确认邮件
await sendOrderConfirmation(order);
res.json({ success: true });
});
Actions 使用自定义变更和查询扩展 GraphQL:
GraphQL SDL 定义:
type Mutation {
login(username: String!, password: String!): LoginResponse
}
type LoginResponse {
accessToken: String!
refreshToken: String!
user: User!
}
Action 配置:
- name: login
definition:
kind: synchronous
handler: https://myapp.com/actions/login
forward_client_headers: true
headers:
- name: X-API-Key
value: secret-key
permissions:
- role: anonymous
Express.js 处理程序:
app.post('/actions/login', async (req, res) => {
const { input, session_variables } = req.body;
const { username, password } = input;
// 验证凭据
const user = await validateCredentials(username, password);
if (!user) {
return res.status(401).json({
message: 'Invalid credentials'
});
}
// 生成令牌
const accessToken = generateJWT(user);
const refreshToken = generateRefreshToken(user);
// 返回 Action 响应
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
username: user.username,
email: user.email
}
});
});
控制哪些角色可以执行 Actions:
通过元数据 API:
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: anonymous
同步 Actions:
异步 Actions:
支付处理:
type 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 编码
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!
}
集成外部 GraphQL API:
通过元数据 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
}
}
}
自定义类型和字段名称以避免冲突:
{
"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]
}
为 'public' 角色限制的模式:
type User {
first_name: String!
last_name: String!
}
type Query {
get_users_by_name(first_name: String!, last_name: String): [User]
}
通过元数据 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] }"
}
}
}
自动将会话变量注入到远程模式查询中:
会话变量预设:
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))
}
将本地数据库表连接到远程模式:
示例:将本地客户链接到远程支付 API
SQL 表:
CREATE TABLE customer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
远程模式(支付 API):
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: $id
带有远程关系的 GraphQL 查询:
query {
customer {
name
customer_transactions_history {
amount
time
}
}
}
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:
hasura-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hasura
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: hasura
template:
metadata:
labels:
app: hasura
spec:
containers:
- name: hasura
image: hasura/graphql-engine:v2.36.0
ports:
- containerPort: 8080
env:
- name: HASURA_GRAPHQL_DATABASE_URL
valueFrom:
secretKeyRef:
name: hasura-secrets
key: database-url
- name: HASURA_GRAPHQL_ADMIN_SECRET
valueFrom:
secretKeyRef:
name: hasura-secrets
key: admin-secret
- name: HASURA_GRAPHQL_JWT_SECRET
valueFrom:
secretKeyRef:
name: hasura-secrets
key: jwt-secret
- name: HASURA_GRAPHQL_ENABLE_CONSOLE
value: "false"
- name: HASURA_GRAPHQL_ENABLE_TELEMETRY
value: "false"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: hasura
namespace: production
spec:
type: ClusterIP
selector:
app: hasura
ports:
- port: 80
targetPort: 8080
基本生产配置:
# 数据库
HASURA_GRAPHQL_DATABASE_URL=postgres://user:password@host:5432/dbname
# 安全性
HASURA_GRAPHQL_ADMIN_SECRET=strong-random-secret
HASURA_GRAPHQL_JWT_SECRET='{"type":"RS256","key":"..."}'
HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous
# 性能
HASURA_GRAPHQL_ENABLE_CONSOLE=false
HASURA_GRAPHQL_DEV_MODE=false
HASURA_GRAPHQL_ENABLE_TELEMETRY=false
# 日志记录
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log
# 速率限制
HASURA_GRAPHQL_RATE_LIMIT_PER_MINUTE=1000
# CORS
HASURA_GRAPHQL_CORS_DOMAIN=https://myapp.com,https://admin.myapp.com
# 连接
HASURA_GRAPHQL_PG_CONNECTIONS=50
HASURA_GRAPHQL_PG_TIMEOUT=60
健康检查端点:
curl http://hasura:8080/healthz
# 返回:OK
Prometheus 指标:
HASURA_GRAPHQL_ENABLE_METRICS=true
HASURA_GRAPHQL_METRICS_SECRET=metrics-secret
# 访问地址:http://hasura:8080/v1/metrics
结构化日志记录:
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log,query-log
HASURA_GRAPHQL_LOG_LEVEL=info
APM 集成(Datadog 示例):
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"
初始化 Hasura 项目:
hasura init my-project --endpoint https://hasura.myapp.com
cd my-project
项目结构:
my-project/
├── config.yaml # Hasura CLI 配置
├── metadata/ # 元数据文件
│ ├── databases/
│ │ └── default/
│ │ ├── tables/
│ │ │ ├── public_users.yaml
│ │ │ └── public_posts.yaml
│ ├── actions.yaml
│ ├── remote_schemas.yaml
│ └── version.yaml
└── migrations/ # 数据库迁移
└── default/
├── 1642531200000_create_users_table/
│ └── up.sql
└── 1642531300000_create_posts_table/
└── up.sql
通过控制台(自动跟踪):
# 启动带有迁移跟踪的控制台
hasura console
# 在控制台 UI 中进行更改
# 迁移自动生成在 migrations/ 文件夹中
手动迁移:
# 创建迁移
hasura migrate create create_users_table --database-name default
# 编辑生成的 SQL 文件
# migrations/default/{timestamp}_create_users_table/up.sql
# migrations/default/{timestamp}_create_users_table/down.sql
示例迁移(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);
示例迁移(down.sql):
DROP TABLE IF EXISTS public.users CASCADE;
应用迁移:
# 应用所有待处理的迁移
hasura migrate apply --database-name default
# 应用特定版本
hasura migrate apply --version 1642531200000 --database-name default
# 检查迁移状态
hasura migrate status --database-name default
导出元数据:
hasura metadata export
# 导出到 metadata/ 文件夹
应用元数据:
hasura metadata apply
# 从 metadata/ 文件夹应用元数据
重新加载元数据:
hasura metadata reload
GitHub Actions 示例:
name: Deploy Hasura
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Hasura CLI
run: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
- name: Apply Migrations
env:
HASURA_GRAPHQL_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
HASURA_GRAPHQL_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}
run: |
cd hasura
hasura migrate apply --database-name default
hasura metadata apply
- name: Reload Metadata
env:
HASURA_GRAPHQL_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
HASURA_GRAPHQL_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}
run: |
cd hasura
hasura metadata reload
在生产环境中始终使用 ADMIN_SECRET
实施适当的 JWT 验证
应用最小权限原则
在生产环境中禁用控制台
HASURA_GRAPHQL_ENABLE_CONSOLE=false启用速率限制
验证 Webhook 负载
优化数据库查询
使用查询缓存
限制查询深度和复杂度
配置连接池
HASURA_GRAPHQL_PG_CONNECTIONS优化订阅
版本控制元数据
环境分离
测试策略
文档
监控和告警
模式:
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)
);
权限(users 表):
{
"filter": {
"organization_id": {
"_eq": "X-Hasura-Org-Id"
}
}
}
JWT 声明:
{
"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),
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.
Use Hasura GraphQL Engine when:
Hasura's primary value proposition is automatic GraphQL API generation from your database schema:
How it works:
query - Fetch data with filtering, sorting, paginationmutation - Insert, update, delete operationssubscription - Real-time data updates via WebSocketsHasura is metadata-driven , not code-driven:
Key metadata components:
Hasura's permission system is its most powerful feature, enabling fine-grained access control:
Permission Types:
select - Read permissionsinsert - Create permissionsupdate - Modify permissionsdelete - Remove permissionsHasura delegates authentication to your auth service but handles authorization :
x-hasura-role, x-hasura-user-id, custom claimsAuth Flow:
Event Triggers enable event-driven architectures by invoking webhooks on database changes:
Actions extend Hasura with custom business logic:
Common use cases:
Remote Schemas enable schema stitching by merging external GraphQL APIs:
Hasura provides native GraphQL subscriptions :
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
}
}
]
}
}
Control which columns are visible per role:
Example: Hide sensitive user fields
select:
columns:
- id
- username
- email
# password_hash is hidden
# created_at is hidden
Example: Different views for different roles
# Admin role sees all columns
select:
columns: "*"
# User role sees limited columns
select:
columns:
- id
- username
- profile_picture
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"
}
}
}
}
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
Control which rows can be deleted:
Example: Delete own data only
{
"filter": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
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 assumex-hasura-user-id for permission checksAuth0 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 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
}
});
}
Alternative to JWT - Hasura calls your webhook for each request:
Webhook endpoint:
app.post('/auth-webhook', async (req, res) => {
const authHeader = req.headers['authorization'];
// Validate token (your logic)
const user = await validateToken(authHeader);
if (!user) {
return res.status(401).json({ message: 'Unauthorized' });
}
// Return session variables
res.json({
'X-Hasura-User-Id': user.id,
'X-Hasura-Role': user.role,
'X-Hasura-Org-Id': user.orgId
});
});
Hasura config:
HASURA_GRAPHQL_AUTH_HOOK=https://myapp.com/auth-webhook
HASURA_GRAPHQL_AUTH_HOOK_MODE=POST
Event triggers invoke webhooks on database changes:
Via Console:
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
}
}
}
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"
}
}
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 extend GraphQL with custom mutations and queries:
GraphQL SDL Definition:
type Mutation {
login(username: String!, password: String!): LoginResponse
}
type LoginResponse {
accessToken: String!
refreshToken: String!
user: User!
}
Action Configuration:
- name: login
definition:
kind: synchronous
handler: https://myapp.com/actions/login
forward_client_headers: true
headers:
- name: X-API-Key
value: secret-key
permissions:
- role: anonymous
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
}
});
});
Control which roles can execute actions:
Via Metadata API:
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "create_action_permission",
"args": {
"action": "insertAuthor",
"role": "user"
}
}
Multiple Roles:
permissions:
- role: user
- role: admin
- role: anonymous
Synchronous Actions:
Asynchronous Actions:
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!
}
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
}
}
}
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_"
}
]
}
}
}
}
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] }"
}
}
}
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))
}
Connect local database tables to remote schemas:
Example: Link local customer to remote payments API
SQL Table:
CREATE TABLE customer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
Remote Schema (Payments API):
type Transaction {
customer_id: Int!
amount: Int!
time: String!
merchant: String!
}
type Query {
transactions(customer_id: String!, limit: Int): [Transaction]
}
Remote Relationship Definition:
- table:
name: customer
schema: public
remote_relationships:
- name: customer_transactions_history
definition:
remote_schema: payments
hasura_fields:
- id
remote_field:
transactions:
arguments:
customer_id: $id
GraphQL Query with Remote Relationship:
query {
customer {
name
customer_transactions_history {
amount
time
}
}
}
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:
hasura-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hasura
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: hasura
template:
metadata:
labels:
app: hasura
spec:
containers:
- name: hasura
image: hasura/graphql-engine:v2.36.0
ports:
- containerPort: 8080
env:
- name: HASURA_GRAPHQL_DATABASE_URL
valueFrom:
secretKeyRef:
name: hasura-secrets
key: database-url
- name: HASURA_GRAPHQL_ADMIN_SECRET
valueFrom:
secretKeyRef:
name: hasura-secrets
key: admin-secret
- name: HASURA_GRAPHQL_JWT_SECRET
valueFrom:
secretKeyRef:
name: hasura-secrets
key: jwt-secret
- name: HASURA_GRAPHQL_ENABLE_CONSOLE
value: "false"
- name: HASURA_GRAPHQL_ENABLE_TELEMETRY
value: "false"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: hasura
namespace: production
spec:
type: ClusterIP
selector:
app: hasura
ports:
- port: 80
targetPort: 8080
Essential Production Config:
# Database
HASURA_GRAPHQL_DATABASE_URL=postgres://user:password@host:5432/dbname
# Security
HASURA_GRAPHQL_ADMIN_SECRET=strong-random-secret
HASURA_GRAPHQL_JWT_SECRET='{"type":"RS256","key":"..."}'
HASURA_GRAPHQL_UNAUTHORIZED_ROLE=anonymous
# Performance
HASURA_GRAPHQL_ENABLE_CONSOLE=false
HASURA_GRAPHQL_DEV_MODE=false
HASURA_GRAPHQL_ENABLE_TELEMETRY=false
# Logging
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log
# Rate Limiting
HASURA_GRAPHQL_RATE_LIMIT_PER_MINUTE=1000
# CORS
HASURA_GRAPHQL_CORS_DOMAIN=https://myapp.com,https://admin.myapp.com
# Connections
HASURA_GRAPHQL_PG_CONNECTIONS=50
HASURA_GRAPHQL_PG_TIMEOUT=60
Health Check Endpoint:
curl http://hasura:8080/healthz
# Returns: OK
Prometheus Metrics:
HASURA_GRAPHQL_ENABLE_METRICS=true
HASURA_GRAPHQL_METRICS_SECRET=metrics-secret
# Access at: http://hasura:8080/v1/metrics
Structured Logging:
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup,http-log,webhook-log,websocket-log,query-log
HASURA_GRAPHQL_LOG_LEVEL=info
APM Integration (Datadog example):
env:
- name: HASURA_GRAPHQL_ENABLE_APM
value: "true"
- name: DD_AGENT_HOST
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: DD_SERVICE
value: "hasura-graphql"
- name: DD_ENV
value: "production"
Initialize Hasura project:
hasura init my-project --endpoint https://hasura.myapp.com
cd my-project
Project structure:
my-project/
├── config.yaml # Hasura CLI config
├── metadata/ # Metadata files
│ ├── databases/
│ │ └── default/
│ │ ├── tables/
│ │ │ ├── public_users.yaml
│ │ │ └── public_posts.yaml
│ ├── actions.yaml
│ ├── remote_schemas.yaml
│ └── version.yaml
└── migrations/ # Database migrations
└── default/
├── 1642531200000_create_users_table/
│ └── up.sql
└── 1642531300000_create_posts_table/
└── up.sql
Via Console (auto-tracked):
# Start console with migration tracking
hasura console
# Make changes in console UI
# Migrations auto-generated in migrations/ folder
Manual migration:
# Create migration
hasura migrate create create_users_table --database-name default
# Edit generated SQL files
# migrations/default/{timestamp}_create_users_table/up.sql
# migrations/default/{timestamp}_create_users_table/down.sql
Example migration (up.sql):
CREATE TABLE public.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON public.users(email);
CREATE INDEX idx_users_username ON public.users(username);
Example migration (down.sql):
DROP TABLE IF EXISTS public.users CASCADE;
Apply migrations:
# Apply all pending migrations
hasura migrate apply --database-name default
# Apply specific version
hasura migrate apply --version 1642531200000 --database-name default
# Check migration status
hasura migrate status --database-name default
Export metadata:
hasura metadata export
# Exports to metadata/ folder
Apply metadata:
hasura metadata apply
# Applies metadata from metadata/ folder
Reload metadata:
hasura metadata reload
GitHub Actions example:
name: Deploy Hasura
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Hasura CLI
run: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
- name: Apply Migrations
env:
HASURA_GRAPHQL_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
HASURA_GRAPHQL_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}
run: |
cd hasura
hasura migrate apply --database-name default
hasura metadata apply
- name: Reload Metadata
env:
HASURA_GRAPHQL_ENDPOINT: ${{ secrets.HASURA_ENDPOINT }}
HASURA_GRAPHQL_ADMIN_SECRET: ${{ secrets.HASURA_ADMIN_SECRET }}
run: |
cd hasura
hasura metadata reload
Always use ADMIN_SECRET in production
Implement proper JWT validation
Apply least-privilege permissions
Disable console in production
HASURA_GRAPHQL_ENABLE_CONSOLE=falseEnable rate limiting
Validate webhook payloads
Optimize database queries
Use query caching
Limit query depth and complexity
Configure connection pooling
HASURA_GRAPHQL_PG_CONNECTIONSOptimize subscriptions
Version control metadata
Environment separation
Testing strategy
Documentation
Monitoring and alerting
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"
}
}
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"
}
}
}
}
]
}
}
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
}
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
}
}
Custom SQL Function for analytics:
CREATE OR REPLACE FUNCTION get_user_stats(user_row users)
RETURNS TABLE (
total_posts INT,
total_followers INT,
total_following INT,
engagement_rate DECIMAL
) AS $$
SELECT
(SELECT COUNT(*) FROM posts WHERE user_id = user_row.id)::INT,
(SELECT COUNT(*) FROM follows WHERE following_id = user_row.id)::INT,
(SELECT COUNT(*) FROM follows WHERE follower_id = user_row.id)::INT,
(SELECT AVG(like_count) FROM posts WHERE user_id = user_row.id)::DECIMAL
$$ LANGUAGE SQL STABLE;
Track function in Hasura:
- function:
name: get_user_stats
schema: public
configuration:
custom_root_fields:
function: getUserStats
GraphQL Query:
query UserWithStats {
users {
id
username
get_user_stats {
total_posts
total_followers
total_following
engagement_rate
}
}
}
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 configured
Issue: 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 permissions
Issue: 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 enabled
Issue: 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 action
Issue: 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 conflicts
Issue: 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 limits
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
Weekly Installs
78
Repository
GitHub Stars
47
First Seen
Jan 22, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykFail
Installed on
codex66
opencode64
cursor63
gemini-cli62
github-copilot60
amp51
lark-cli 共享规则:飞书资源操作指南与权限配置详解
39,000 周安装
AI引导抗体工程优化:从先导物到临床候选物的全流程解决方案
158 周安装
iOS Core Location 问题诊断指南 - 位置更新、后台定位、授权问题排查
159 周安装
Tone.js 教程:使用 Web Audio API 在浏览器中构建交互式音乐应用
160 周安装
sciomc:AI驱动的并行研究代理,自动化分解与验证复杂研究目标
168 周安装
Playwright CLI:无需编码的浏览器自动化测试工具 - 快速上手与安全指南
161 周安装
Spec测试套件生成工具 - 自动化编排smoke/regression/targeted测试用例,提升软件质量
70 周安装