H
×
🥗

NutriTrack — API GraphQL en tiempo real con Hasura

Guía técnica completa para el dev team · PostgreSQL 16 + Hasura v2.42 + React
GraphQL PostgreSQL 16 Subscriptions en tiempo real Row-Level Security Actions · Event Triggers Hasura Cloud
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
Frontend
Hasura
GraphQL Engine
Capa API
🐘
PostgreSQL 16
Railway
Base de datos
🔧
Node.js API
Actions Handler
Lógica custom
📡
Webhooks
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 Auth0
Añadir Action en Auth0 Rules para inyectar el claim custom
Aplicar permisos YAML para los 2 roles (employee, hr_admin)
hasura metadata apply en staging
Deploy handler de Action approveMealLogs en Railway
Actualizar 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 React
Verificar 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=100
Pipeline CI/CD: hasura migrate apply && hasura metadata apply
Metadata 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.