Skip to content
Francisca Portugal edited this page Nov 2, 2026 · 1 revision

🚧 Not implemented on the new backend. There is no email dependency, sender, template or env var in apps/backend/src. This page records what the legacy backend sent and what a replacement needs — it is not a working integration guide.

Today

Question Answer
Does a /v1 route send email? No
Is Resend installed? No — no dependency, env var or call
Is there a sender/template? No
Does the frontend still verify exchange links? Yes — /exchange/verify/:token exists, for links the legacy backend generated
What happens if an exchange is created on the new backend? Nothing is sent; there are no exchange endpoints yet

exchange.verify and the hash column exist in the schema for this flow, but nothing consumes them.

What the legacy backend did

The Django app (apps/backend/legacy/) used Mailjet via django-anymail in production and SMTP in debug:

if DEBUG:
    EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"   # mailpit at tts-mailpit:1025
else:
    EMAIL_BACKEND = "anymail.backends.mailjet.EmailBackend"

DEFAULT_FROM_EMAIL = CONFIG['SENDER_EMAIL_ADDRESS']

Emails were sent on:

  • direct exchange creation — one per participant;
  • revalidation — resend the confirmation link.

The message was Confirmação de troca, sent to up<nmec>@up.pt. It contained a JWT signed with HS256 (username, exchange_id, exp), base64-encoded into a link:

{DOMAIN}/exchange/verify/{base64(jwt)}

The token expiry was VERIFY_EXCHANGE_TOKEN_EXPIRATION_SECONDS. Failures were caught and printed; the API still answered success, so “request created” never meant “email delivered”.

Relevant legacy env vars: MAILJET_API_KEY, MAILJET_SECRET_KEY, SENDER_EMAIL_ADDRESS, STUDENT_EMAIL_DOMAIN, EMAIL_HOST/PORT for mailpit.

What the replacement needs

Decision record: 0016. Resend for email.

Minimum pieces:

  1. Provider — Resend was chosen in the tech-stack for its fetch-based SDK and free tier.
  2. Config — RESEND_API_KEY (sensitive), EMAIL_FROM, and a public FRONTEND_URL/DOMAIN for links. None exist in .env.schema today.
  3. Port — EmailSender in domain/ports/ with a Resend adapter in infrastructure/external/, so use-cases do not import the SDK.
  4. Templates — verification, revalidation; rendered server-side or linked from the frontend.
  5. Tokens — the schema already has ExchangeRequest.hash; use an unguessable token with an expiry, and store a hash — not the raw token.
  6. Triggers — on exchange creation/revalidation (and any later claim/accept flow).
  7. Failure handling — a failed send must be visible (log/metric) and retryable; do not silently swallow it like the legacy code.
  8. Tests — a fake EmailSender in route tests, asserting the use-case called it; no real network.

Security notes

  • Email links are bearer capabilities. Anyone with the link can act; use high-entropy tokens, short expiry, and single use.
  • Base64 is not encryption. The legacy JWT is readable in the URL; do not put PII in it.
  • Tokens leak through history, referrers and forwarded mail. Keep them short-lived.
  • Do not log the token or the full email body.
  • Rate-limit resends so an endpoint cannot be used to spam a student.

What to do until it exists

  • Do not promise email delivery in UI copy for new exchange flows.
  • Keep the legacy verification page working for legacy links, but treat it as legacy.
  • When building exchanges on /v1, either include the notification in the same scope or ship the feature with an explicit “no email yet” limitation.

See also


Last verified: 2026-11-02

Clone this wiki locally