Ir al contenido

Email (Resend)

CÉNIT envía emails transaccionales con la API de Resendno es SMTP. La función central es sendEmail() en lib/send-email.ts. Si RESEND_API_KEY no está configurada en Workers Secrets, el envío falla silenciosamente (devuelve false) y el error queda capturado en Sentry. Tanto la invitación de staff como la de jugador usan el mismo patrón: auth.admin.generateLink() para generar el link, y sendEmail() (Resend) para entregarlo — el SMTP nativo de Supabase no se usa.

POST directo a https://api.resend.com/emails con Authorization: Bearer ${RESEND_API_KEY} y body { from: RESEND_FROM, to, subject, html }.

  • Si Resend responde 2xx → retorna true.
  • Si responde 4xx/5xx → Sentry.captureException con tags email_to, email_subject y extras resend_status, resend_body. Retorna false. No tira excepción — el caller decide si reintenta o se rinde.

lib/send-email.ts exporta dos generadores:

  • inviteEmailHtml({ actionLink, forPlayer, forRecovery, orgName, orgLogoUrl }) — email transaccional principal (invitación staff, invitación jugador y recovery de contraseña). Soporta branding por org: logo y nombre. Seguridad: orgName y orgLogoUrl se escapan (escapeHtml, safeUrl) antes de interpolarse — un admin malicioso no puede inyectar <script> ni javascript: URLs.
  • superadminMagicLinkHtml({ actionLink }) — email del panel superadmin interno (@cenitams.com). Sin branding de club.

Settings → Usuarios → InvitarinviteOrgUser(email, role)supabase.auth.admin.generateLink({ type: 'invite' }) (con fallback a magiclink si el usuario ya existe) → sendEmail() de Resend. Si RESEND_API_KEY no está configurada el envío falla silencioso.

Plantel → [Jugador] → InvitarinvitePlayer(playerId, email) en plantel/actions.tsadmin.auth.admin.generateLink({ type: 'invite' })

  • sendEmail() con inviteEmailHtml({ forPlayer: true }). Si RESEND_API_KEY falta, falla silencioso — el log queda en Sentry, pero el staff puede no enterarse.

Disparado desde /superadmin/loginsendEmail() + superadminMagicLinkHtml(). Link expira en 1 hora.

Ventana de terminal
RESEND_API_KEY= # bloqueador crítico — sin esto, no se envían mails
RESEND_FROM= # dirección "From:" (debe ser un dominio verificado en Resend)
NEXT_PUBLIC_SITE_URL=https://cenitams.com

Configurar en Workers Secrets (wrangler secret put RESEND_API_KEY), no en wrangler.toml plano.

  • Sentry — todo error de envío se reporta con tags identificatorios.
  • Multi-tenantorgName y orgLogoUrl provienen de la fila organizations del invitado, se escapan antes de renderizar.
  • Plans y billing — no relevante directo; los planes no afectan el envío de mails.
  • Falla silenciosa — si RESEND_API_KEY no está, sendEmail() retorna false sin avisar a la UI. Riesgo: el staff cree que invitó al jugador pero el mail nunca salió. Mitigación actual: monitoreo en Sentry. Mejora futura: surfacear el error en la UI.
  • Sin queue ni retry — un error transitorio de Resend pierde el mail. Para uso real esto rara vez pasa, pero un retry exponencial con backoff sería más robusto.
  • Sin email del jugador en producción para Nacional — el flow legacy /w/[token] no necesita email; el flow nuevo de onboarding masivo sí. Si el PF no pre-cargó emails, hay un fallback manual.