Notificaciones
Las notificaciones informan a cada usuario sobre eventos relevantes del club: recordatorios de turnos, tareas vencidas, vencimientos de membresías, novedades de cultivos, órdenes y avisos del sistema. En esta página vamos a recorrer los distintos endpoints BFF de notificaciones que expone la webapp de Cannahub. Veremos cómo listar las notificaciones del usuario autenticado, marcarlas como leídas, obtener el conteo de no leídas y generar recordatorios automáticos.
Todas las rutas son rutas BFF propias de la webapp (/api/notifications/...), que internamente delegan al servicio de notificaciones (Strapi). El acceso se resuelve a partir del usuario autenticado: cada usuario solo puede operar sobre sus propias notificaciones (su recipientId). El multi-tenant se resuelve a través del header Tenant-id.
El modelo de notificación
El modelo de notificación describe un aviso dirigido a un destinatario (recipientId) dentro de un tenant. Incluye el dominio funcional, el evento que la originó, un título y cuerpo legibles, la severidad, el estado de lectura y metadata adicional.
Propiedades
- Name
id- Type
- string
- Description
Identificador interno de la notificación.
- Name
externalId- Type
- string
- Description
Identificador externo estable de la notificación, usado por el cliente para marcarla como leída.
- Name
tenantId- Type
- string
- Description
Identificador del tenant (club) al que pertenece la notificación.
- Name
recipientId- Type
- string
- Description
Identificador del destinatario de la notificación (el usuario que la recibe).
- Name
recipientRole- Type
- string
- Description
Rol del destinatario al momento de generarse la notificación (por ejemplo
member,grower,admin).
- Name
domain- Type
- enum
- Description
Dominio funcional de la notificación. Puede ser uno de:
- orders
- appointments
- crops
- tasks
- memberships
- medical
- products
- inventory
- workforce
- documents
- system
- Name
event- Type
- string
- Description
Evento que originó la notificación (por ejemplo
appointment.reminder,task.overdue).
- Name
title- Type
- string
- Description
Título legible de la notificación.
- Name
body- Type
- string
- Description
Cuerpo o descripción de la notificación.
- Name
severity- Type
- enum
- Description
Severidad de la notificación. Puede ser una de:
- info
- success
- warning
- error
- Name
actionUrl- Type
- string
- Description
URL de acción asociada a la notificación (por ejemplo, el detalle del turno o de la tarea).
- Name
resourceId- Type
- string
- Description
Identificador del recurso relacionado (turno, tarea, orden, etc.).
- Name
resourceType- Type
- string
- Description
Tipo del recurso relacionado.
- Name
isRead- Type
- boolean
- Description
Indica si la notificación fue leída.
- Name
readAt- Type
- string
- Description
Timestamp de cuándo se marcó como leída.
- Name
metadata- Type
- object
- Description
Datos adicionales arbitrarios asociados a la notificación.
- Name
source- Type
- enum
- Description
Origen de la notificación. Puede ser uno de:
- event: Generada por un evento del sistema
- reminder: Recordatorio programado (turnos, tareas vencidas)
- manual: Creada manualmente
- system: Aviso del sistema
- Name
expiresAt- Type
- string
- Description
Timestamp de expiración. Las notificaciones vencidas se limpian automáticamente.
- Name
createdAt- Type
- string
- Description
Timestamp de creación de la notificación.
- Name
updatedAt- Type
- string
- Description
Timestamp de la última actualización de la notificación.
Ejemplo de notificación
{
"id": "12345",
"externalId": "ntf_01HQ9REMIND001",
"tenantId": "tnt_01HQ8CLUB001",
"recipientId": "usr_01HQ8XYZABC123",
"recipientRole": "member",
"domain": "appointments",
"event": "appointment.reminder",
"title": "Recordatorio de turno",
"body": "Tenés un turno mañana a las 15:00 hs en el club.",
"severity": "info",
"actionUrl": "/appointments/apt_01HQ9APT001",
"resourceId": "apt_01HQ9APT001",
"resourceType": "appointment",
"isRead": false,
"readAt": null,
"metadata": {
"date": "2026-09-10",
"time": "15:00"
},
"source": "reminder",
"expiresAt": "2026-09-11T00:00:00Z",
"createdAt": "2026-09-09T09:00:00Z",
"updatedAt": "2026-09-09T09:00:00Z"
}
Listar notificaciones
Retorna una lista paginada de las notificaciones del usuario autenticado. El destinatario (recipientId) se resuelve automáticamente a partir de la sesión, por lo que cada usuario solo recibe sus propias notificaciones. Podés filtrar por dominio, estado de lectura y origen.
Query params opcionales
- Name
domain- Type
- enum
- Description
Filtra por dominio funcional (
orders,appointments,crops,tasks,memberships,medical,products,inventory,workforce,documents,system).
- Name
isRead- Type
- boolean
- Description
Filtra por estado de lectura. Usá
truepara leídas ofalsepara no leídas.
- Name
source- Type
- enum
- Description
Filtra por origen de la notificación (
event,reminder,manual,system).
- Name
page- Type
- integer
- Description
Número de página. Por defecto
1.
- Name
limit- Type
- integer
- Description
Cantidad de notificaciones por página. Por defecto
20.
Request
curl -G https://api.cannahub.tech/api/notifications \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-d domain=appointments \
-d isRead=false \
-d page=1 \
-d limit=20
Response
{
"notifications": [
{
"id": "12345",
"externalId": "ntf_01HQ9REMIND001",
"tenantId": "tnt_01HQ8CLUB001",
"recipientId": "usr_01HQ8XYZABC123",
"recipientRole": "member",
"domain": "appointments",
"event": "appointment.reminder",
"title": "Recordatorio de turno",
"body": "Tenés un turno mañana a las 15:00 hs en el club.",
"severity": "info",
"actionUrl": "/appointments/apt_01HQ9APT001",
"resourceId": "apt_01HQ9APT001",
"resourceType": "appointment",
"isRead": false,
"source": "reminder",
"createdAt": "2026-09-09T09:00:00Z",
"updatedAt": "2026-09-09T09:00:00Z"
},
{
"id": "12346",
"externalId": "ntf_01HQ9MEMBER07",
"tenantId": "tnt_01HQ8CLUB001",
"recipientId": "usr_01HQ8XYZABC123",
"recipientRole": "member",
"domain": "memberships",
"event": "membership.expiring",
"title": "Tu membresía está por vencer",
"body": "Tu membresía Premium vence el 15/01. Renovala para no perder acceso.",
"severity": "warning",
"actionUrl": "/membership",
"resourceId": "mbr_01HQ9ABC456",
"resourceType": "membership",
"isRead": false,
"source": "reminder",
"createdAt": "2026-09-08T09:00:00Z",
"updatedAt": "2026-09-08T09:00:00Z"
}
],
"count": 2,
"pagination": {
"page": 1,
"limit": 20,
"total": 2
}
}
Conteo de no leídas
Retorna la cantidad de notificaciones no leídas del usuario autenticado. Es una ruta liviana pensada para alimentar el badge del ícono de notificaciones. En la webapp se refresca periódicamente (cada 30 segundos) y al recuperar el foco de la ventana.
El recipientId se resuelve a partir de la sesión; no requiere ningún parámetro adicional.
Response
- Name
count- Type
- number
- Description
Cantidad de notificaciones no leídas del usuario.
Request
curl https://api.cannahub.tech/api/notifications/unread-count \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"count": 5
}
Marcar como leídas
Marca una notificación específica como leída, o bien todas las notificaciones del usuario autenticado. El body debe incluir o notificationId o all: true. Si no se envía ninguno de los dos, la ruta responde con un error de validación.
Para marcar una notificación individual usá su externalId como notificationId.
Body
- Name
notificationId- Type
- string
- Description
Identificador (
externalId) de la notificación a marcar como leída. Requerido si no se envíaall.
- Name
all- Type
- boolean
- Description
Si es
true, marca como leídas todas las notificaciones del usuario. Requerido si no se envíanotificationId.
Request
curl -X POST https://api.cannahub.tech/api/notifications/mark-read \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"notificationId": "ntf_01HQ9REMIND001"
}'
Response (notificationId)
{
"notification": {
"id": "12345",
"externalId": "ntf_01HQ9REMIND001",
"tenantId": "tnt_01HQ8CLUB001",
"recipientId": "usr_01HQ8XYZABC123",
"recipientRole": "member",
"domain": "appointments",
"event": "appointment.reminder",
"title": "Recordatorio de turno",
"severity": "info",
"isRead": true,
"readAt": "2026-09-09T12:30:00Z",
"source": "reminder",
"createdAt": "2026-09-09T09:00:00Z",
"updatedAt": "2026-09-09T12:30:00Z"
}
}
Response (all: true)
{
"updated": 5
}
Generar recordatorios
Endpoint programado (cron) que genera notificaciones de recordatorio para todos los tenants. No usa autenticación de usuario: se protege con el secreto CRON_API_SECRET, que se envía en el header Authorization como Bearer {CRON_API_SECRET}. Se ejecuta diariamente (a las 09:00 UTC) desde un GitHub Action.
En cada corrida, por cada tenant, la ruta:
- Genera recordatorios de turnos para los turnos confirmados del día siguiente (
appointment.reminder). - Genera recordatorios de tareas vencidas para tareas
PENDINGoIN_PROGRESScon vencimiento hasta hoy (task.overdue). - Limpia las notificaciones vencidas (según su
expiresAt).
Incluye control de idempotencia: si ya existe un recordatorio del mismo evento y recurso generado hoy para el destinatario, se omite.
Esta ruta está pensada para ser invocada por el sistema de cron, no desde el cliente. En modo mock retorna { "message": "Skipped in mock mode", "tenants": 0, "reminders": {} }.
Autenticación
- Name
Authorization- Type
- header
- Description
Debe ser
Bearer {CRON_API_SECRET}. Cualquier otro valor responde401 Unauthorized.
Response
- Name
tenants- Type
- number
- Description
Cantidad de tenants procesados.
- Name
reminders- Type
- object
- Description
Cantidad de recordatorios generados.
- Name
appointments- Type
- number
- Description
Recordatorios de turnos generados.
- Name
tasks- Type
- number
- Description
Recordatorios de tareas vencidas generados.
- Name
expired- Type
- number
- Description
Cantidad de notificaciones vencidas eliminadas.
Request
curl -X POST https://api.cannahub.tech/api/notifications/generate-reminders \
-H "Authorization: Bearer {CRON_API_SECRET}"
Response
{
"tenants": 3,
"reminders": {
"appointments": 12,
"tasks": 5
},
"expired": 8
}
React Query Hooks
La webapp de Cannahub provee hooks de React Query pre-armados para gestionar notificaciones. Estos hooks consumen las rutas BFF y manejan el cacheo, la invalidación y las actualizaciones optimistas automáticamente.
Query Keys
// features/shared/notifications/hooks/useNotifications.ts
export const notificationKeys = {
all: ['notifications'] as const,
lists: () => [...notificationKeys.all, 'list'] as const,
list: (filters: StrapiNotificationFilters) => [...notificationKeys.lists(), filters] as const,
unreadCount: () => [...notificationKeys.all, 'unread-count'] as const,
}
Queries
import { useNotifications, useUnreadCount } from '@/features/shared/notifications/hooks/useNotifications'
// Listado paginado (useInfiniteQuery) con filtros opcionales
const { data, fetchNextPage, hasNextPage } = useNotifications({
domain: 'appointments',
isRead: false,
limit: 20,
})
// Conteo de no leídas (para el badge). Refresca cada 30s.
const { data: unread } = useUnreadCount()
Mutations
import { useMarkAsRead, useMarkAllAsRead } from '@/features/shared/notifications/hooks/useNotifications'
// Marcar una notificación como leída (por externalId)
const { mutate: markAsRead } = useMarkAsRead()
markAsRead('ntf_01HQ9REMIND001')
// Marcar todas como leídas
const { mutate: markAllAsRead } = useMarkAllAsRead()
markAllAsRead()
Actualizaciones optimistas
Las mutaciones useMarkAsRead y useMarkAllAsRead aplican cambios optimistas en el cache antes de que responda el servidor: actualizan el estado isRead de las notificaciones en el listado y decrementan (o ponen en cero) el conteo de no leídas. Si la request falla, se hace rollback al estado previo. Al finalizar, se invalidan las queries relevantes:
queryClient.invalidateQueries({ queryKey: notificationKeys.lists() })
queryClient.invalidateQueries({ queryKey: notificationKeys.unreadCount() })