Registro de push token + purga de tokens muertos
Objetivo
Mantener actualizado qué dispositivos reciben push de qué usuario, incluyendo cuando un dispositivo cambia de dueño (móvil compartido) o Firebase rota el token.
Cómo empieza
petid-client pide permisos y token nativo al arrancar sesión (registerForPushNotificationsAsync(), (home)/_layout.tsx).
Pasos principales
- Se pide el token FCM crudo (
getDevicePushTokenAsync()), deliberadamente nogetExpoPushTokenAsync()—notification-api/notification-workerhablan con FCM/APNs directo, no reenvían por los servidores de Expo. POST /devices { token, platform }→RegisterDeviceUseCase.execute()→devices.upsertByToken(device).- La identidad es el token, no el par (userId, token): si el token ya existe en otro usuario, se reasigna al usuario actual. Cubre rotación de token y dispositivo compartido sin lógica extra.
- Logout →
DELETE /devices/:token(best-effort, no bloquea el logout si falla). - Cuando
notification-workerdetecta un token muerto (FCM devuelve “unregistered”), llama de vuelta:POST /webhooks/push/dead-tokens, guardado con headerx-webhook-secret(FCM_CLEANER_WEBHOOK_SECRET, comparación timing-safe) →PurgeDeadTokensUseCaseborra esos tokens del registry.
Reglas de negocio
token @uniqueen el schema es lo que hace natural la reasignación — no hace falta lógica manual de “si pertenece a otro usuario, quitárselo primero”.- El webhook de purga es asíncrono respecto al envío:
POST /notifications/pushresponde202antes de saber si el token era válido.
Interacciones
petid-client → petid-api (/devices) ; petid-api → notification-api → FCM/APNs ; notification-worker → petid-api (/webhooks/push/dead-tokens).
Dónde seguir mirando
petid-api/src/application/devices/register-device.use-case.ts,purge-dead-tokens.use-case.tspetid-api/src/presentation/webhooks/webhooks.controller.ts,guards/webhook-secret.guard.tspetid-client/src/services/push-notifications.ts,devices-api.ts
Casos especiales
- Si
FCM_CLEANER_WEBHOOK_SECRETno está configurado, el guard responde401directo — no hay modo “sin verificación” por accidente. - En web y Expo Go, el registro es un no-op silencioso (
Platform.OS === 'web' || isRunningInExpoGo()) — no falla, simplemente no hace nada.