0
resolvers CRUD escritos a mano
4
tablas trackeadas con auto-API
~2h
de setup hasta API funcional
60%
reducción tiempo dev estimada
0
Arquitectura de la solución
React App
Apollo Client
Apollo Client
Frontend
→
Hasura
GraphQL Engine
GraphQL Engine
Capa API
→
PostgreSQL 16
Railway
Railway
Base de datos
→
Node.js API
Actions Handler
Actions Handler
Lógica custom
→
Webhooks
Event Triggers
Event Triggers
Side effects
Hasura actúa como gateway único: el frontend solo habla GraphQL. La lógica de negocio compleja (aprobación de logs, gamificación) vive en microservicios Node que Hasura invoca via Actions y Event Triggers.
1
Schema PostgreSQL — 4 tablas, relaciones auto-detectadas
companies
Plan del cliente corporativo
PKiduuid DEFAULT gen_random_uuid()
nametext NOT NULL
plantext DEFAULT 'starter'
employee_countint DEFAULT 0
created_attimestamptz DEFAULT now()
employees
Empleados de la empresa
PKiduuid DEFAULT gen_random_uuid()
FKcompany_iduuid → companies.id
nametext NOT NULL
emailtext UNIQUE NOT NULL
dietary_profilejsonb
gamification_pointsint DEFAULT 0
meal_plans
Plantillas de plan alimentario
PKiduuid DEFAULT gen_random_uuid()
FKcompany_iduuid → companies.id
nametext NOT NULL
calories_targetint NOT NULL
activeboolean DEFAULT true
created_attimestamptz DEFAULT now()
meal_logs
Registros de comida diarios
PKiduuid DEFAULT gen_random_uuid()
FKemployee_iduuid → employees.id
FKmeal_plan_iduuid → meal_plans.id
logged_attimestamptz DEFAULT now()
caloriesint NOT NULL
approvedboolean DEFAULT false
notestext
bash — Migration
Crear con Hasura CLI (versionado en git)
# Crear migración desde el estado actual de la DB hasura migrate create "init_nutritrack_schema" \ --from-server \ --database-name default \ --endpoint https://nutritrack.hasura.app # Aplicar en staging/producción hasura migrate apply --database-name default --endpoint $HASURA_ENDPOINT hasura metadata apply --endpoint $HASURA_ENDPOINT
2
GraphQL auto-generado — Queries, Mutations y Subscriptions
GraphQL — Query (HR Admin)
Dashboard de empresa con agregados
query GetCompanyDashboard($company_id: uuid!) { employees( where: { company_id: { _eq: $company_id } } order_by: { name: asc } ) { id name email gamification_points # Relación auto-detectada meal_logs_aggregate( where: { approved: { _eq: true } } ) { aggregate { count avg { calories } } } } # Resumen empresa meal_logs_aggregate( where: { employee: { company_id: { _eq: $company_id } } approved: { _eq: false } } ) { aggregate { count } } }
GraphQL — Mutation (Employee)
Registrar comida (con check de permisos)
mutation LogMeal($meal_plan_id: uuid!, $calories: Int!) { insert_meal_logs_one( object: { # employee_id auto-set desde JWT meal_plan_id: $meal_plan_id calories: $calories # approved: false por defecto (solo HR puede aprobar) } ) { id logged_at calories meal_plan { name calories_target } } }
GraphQL — Subscription (tiempo real)
Dashboard HR actualiza en vivo vía WebSocket
subscription PendingMealLogs($company_id: uuid!) { meal_logs( where: { employee: { company_id: { _eq: $company_id } } approved: { _eq: false } } order_by: { logged_at: desc } ) { id logged_at calories notes employee { name email dietary_profile } meal_plan { name calories_target } } } // React: Apollo useSubscription hook const { data, loading } = useSubscription( PENDING_MEAL_LOGS, { variables: { company_id: currentCompany } } ) // Se re-renderiza automáticamente con cada INSERT
Hasura detecta las foreign keys automáticamente y expone las relaciones como campos anidados en GraphQL. No hay N+1: usa JOIN de PostgreSQL bajo el capó.
3
Permisos por rol — Row-Level Security en Hasura
YAML — metadata/databases/default/tables/public_meal_logs.yaml
Permisos completos para meal_logs
table: name: meal_logs schema: public select_permissions: # Employee: solo ve SUS propios logs - role: employee permission: columns: [id, meal_plan_id, logged_at, calories, approved, notes] filter: employee_id: { _eq: X-Hasura-User-Id } limit: 100 allow_aggregations: true # HR Admin: ve TODOS los logs de su empresa - role: hr_admin permission: columns: "*" filter: employee: company_id: { _eq: X-Hasura-Company-Id } allow_aggregations: true insert_permissions: - role: employee permission: columns: [meal_plan_id, calories, notes] set: employee_id: X-Hasura-User-Id # Auto desde JWT approved: false # Forzado: no puede auto-aprobarse check: calories: { _gt: 0, _lte: 5000 } # Validación de rango update_permissions: # Solo HR puede aprobar/rechazar (update via Action, no directo) - role: hr_admin permission: columns: [approved, notes] filter: employee: company_id: { _eq: X-Hasura-Company-Id }
TypeScript — Configurar JWT para Auth0/Supabase Auth
Claims personalizados en el token
// El JWT debe incluir el namespace de Hasura: { "sub": "usr_abc123", "email": "lucia@techcorp.com", "https://hasura.io/jwt/claims": { "x-hasura-default-role": "employee", "x-hasura-allowed-roles": ["employee", "hr_admin"], "x-hasura-user-id": "usr_abc123", "x-hasura-company-id": "cmp_techcorp" // claim custom } }
4
Action — approveMealLogs (lógica de negocio batch)
YAML — metadata/actions.yaml
Definición de la Action
actions: - name: approveMealLogs definition: kind: synchronous handler: http://api:3000/actions/approve-logs type: mutation arguments: - name: log_ids type: [uuid!]! - name: hr_notes type: String output_type: ApproveResult permissions: - role: hr_admin custom_types: objects: - name: ApproveResult fields: - name: approved_count type: Int! - name: points_awarded type: Int!
TypeScript — Action Handler
api/actions/approve-logs.ts
export default async function handler(req: Request) { const { input, session_variables } = await req.json(); const companyId = session_variables["x-hasura-company-id"]; const { log_ids, hr_notes } = input; // 1. Verificar que los logs pertenecen a la empresa del HR const logs = await db.query(` SELECT ml.id FROM meal_logs ml JOIN employees e ON e.id = ml.employee_id WHERE ml.id = ANY($1) AND e.company_id = $2 AND ml.approved = false `, [log_ids, companyId]); if (logs.rows.length === 0) return Response.json({ errors: [{ message: "No logs found" }] }); // 2. Aprobar en batch await db.query(` UPDATE meal_logs SET approved = true, notes = $1 WHERE id = ANY($2) `, [hr_notes, logs.rows.map(r => r.id)]); // 3. Puntos de gamificación: +10 por log aprobado const points = logs.rows.length * 10; return Response.json({ approved_count: logs.rows.length, points_awarded: points }); }
GraphQL — Invocar la Action desde el frontend
HR aprueba 3 registros de una vez
mutation ApproveLogs { approveMealLogs( log_ids: ["a1b2c3...", "d4e5f6...", "g7h8i9..."] hr_notes: "Revisado semana del 9 de junio. Todo correcto." ) { approved_count # → 3 points_awarded # → 30 } }
5
Event Trigger — on_meal_log_approved (gamificación automática)
YAML — Event Trigger config
Dispara webhook al aprobar un log
event_triggers: - name: on_meal_log_approved definition: enable_manual: false update: columns: [approved] retry_conf: num_retries: 3 interval_sec: 15 timeout_sec: 60 webhook: http://api:3000/webhooks/meal-approved headers: - name: x-webhook-secret value_from_env: WEBHOOK_SECRET
TypeScript — Webhook Handler
api/webhooks/meal-approved.ts
export default async function handler(req: Request) { const { event } = await req.json(); const { old: prev, new: curr } = event.data; // Solo cuando cambia approved false → true if (curr.approved && !prev?.approved) { // Sumar 10 puntos al empleado await db.query(` UPDATE employees SET gamification_points = gamification_points + 10 WHERE id = $1 `, [curr.employee_id]); // Notificar al empleado por email await sendEmail({ to: curr.employee_email, subject: "¡Tu registro fue aprobado! +10 pts 🎉", template: "meal-approved", data: { calories: curr.calories, date: curr.logged_at } }); } return Response.json({ success: true }); }
Importante: Event Triggers son idempotentes: el handler puede recibir el mismo evento más de una vez (reintentos). Asegurate de que incrementar puntos sea idempotente usando un campo
last_approved_log_id para detectar duplicados.6
Checklist de despliegue — Dev → Staging → Producción
Setup inicial (done en local)
✓
Docker Compose levantado localmente (Hasura + PostgreSQL 16)
http://localhost:8080/console accesible
✓
4 tablas creadas y trackeadas en Hasura Console
companies, employees, meal_plans, meal_logs
✓
Relaciones FK detectadas y nombradas
employees.company_id → companies, meal_logs.employee_id → employees
Pendiente — Staging
Configurar JWT con claims
x-hasura-company-id en Auth0Añadir Action en Auth0 Rules para inyectar el claim custom
Aplicar permisos YAML para los 2 roles (employee, hr_admin)
hasura metadata apply en stagingDeploy handler de Action
approveMealLogs en RailwayActualizar URL del handler en metadata
Probar Event Trigger con insert/update manual desde Console
Verificar que los reintentos están configurados (3 intentos, 15s intervalo)
Integrar
useSubscription de Apollo en el dashboard ReactVerificar que la suscripción se reconecta automáticamente
Producción (Hasura Cloud)
Crear proyecto en Hasura Cloud — conectar Railway PostgreSQL
Reemplazar admin_secret en CI/CD secrets
Activar Rate Limiting en Hasura Cloud (plan Business)
Max 100 req/min por usuario para evitar abuso de suscripciones
Habilitar query caching para queries frecuentes del dashboard
HASURA_GRAPHQL_QUERY_PLAN_CACHE_SIZE=100Pipeline CI/CD:
hasura migrate apply && hasura metadata applyMetadata y migraciones versionadas en git, nunca cambios manuales en prod
Reglas de oro para este proyecto
Permisos en todas las tablas — Sin permisos definidos, los roles no-admin no ven nada. Definir permisos es parte del schema, no un paso opcional.
Relationships sobre JOINs manuales — Trackear las FKs y usar relaciones anidadas. Hasura resuelve N+1 con JOIN de PostgreSQL automáticamente.
Subscriptions > polling — Para el dashboard de HR, usar
useSubscription de Apollo. Hasura gestiona el WebSocket eficientemente.Admin secret solo para CI/CD — Nunca exponer el admin_secret al frontend. Los usuarios se autentican con JWT (Auth0/Supabase), no con el secret.
Actions para lógica de negocio — Aprobaciones batch, pagos, notificaciones → Actions. No en el cliente. No directamente en mutations.
Migrations en git — Exportar metadata y migrations con la CLI. Deploy via
hasura migrate apply en cada entorno, nunca cambios manuales.