Email (Resend)
CÉNIT envía emails transaccionales con la API de Resend — no 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.
Cómo funciona
Sección titulada «Cómo funciona»sendEmail({ to, subject, html })
Sección titulada «sendEmail({ to, subject, html })»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.captureExceptioncon tagsemail_to,email_subjecty extrasresend_status,resend_body. Retornafalse. No tira excepción — el caller decide si reintenta o se rinde.
Plantillas HTML
Sección titulada «Plantillas HTML»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:orgNameyorgLogoUrlse escapan (escapeHtml,safeUrl) antes de interpolarse — un admin malicioso no puede inyectar<script>nijavascript:URLs.superadminMagicLinkHtml({ actionLink })— email del panel superadmin interno (@cenitams.com). Sin branding de club.
Cómo se usan
Sección titulada «Cómo se usan»Invitación de staff (vía Resend)
Sección titulada «Invitación de staff (vía Resend)»Settings → Usuarios → Invitar → inviteOrgUser(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.
Invitación de jugador (vía Resend)
Sección titulada «Invitación de jugador (vía Resend)»Plantel → [Jugador] → Invitar → invitePlayer(playerId, email) en
plantel/actions.ts → admin.auth.admin.generateLink({ type: 'invite' })
sendEmail()coninviteEmailHtml({ forPlayer: true }). SiRESEND_API_KEYfalta, falla silencioso — el log queda en Sentry, pero el staff puede no enterarse.
Magic-link superadmin
Sección titulada «Magic-link superadmin»Disparado desde /superadmin/login → sendEmail() +
superadminMagicLinkHtml(). Link expira en 1 hora.
Variables de entorno
Sección titulada «Variables de entorno»RESEND_API_KEY= # bloqueador crítico — sin esto, no se envían mailsRESEND_FROM= # dirección "From:" (debe ser un dominio verificado en Resend)NEXT_PUBLIC_SITE_URL=https://cenitams.comConfigurar en Workers Secrets (wrangler secret put RESEND_API_KEY),
no en wrangler.toml plano.
Integraciones
Sección titulada «Integraciones»- Sentry — todo error de envío se reporta con tags identificatorios.
- Multi-tenant —
orgNameyorgLogoUrlprovienen de la filaorganizationsdel invitado, se escapan antes de renderizar. - Plans y billing — no relevante directo; los planes no afectan el envío de mails.
Limitaciones / roadmap
Sección titulada «Limitaciones / roadmap»- Falla silenciosa — si
RESEND_API_KEYno está,sendEmail()retornafalsesin 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.