Repository navigation
🚧 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.
| 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.
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.
Decision record: 0016. Resend for email.
Minimum pieces:
-
Provider — Resend was chosen in the tech-stack for its
fetch-based SDK and free tier. -
Config —
RESEND_API_KEY(sensitive),EMAIL_FROM, and a publicFRONTEND_URL/DOMAINfor links. None exist in.env.schematoday. -
Port —
EmailSenderindomain/ports/with a Resend adapter ininfrastructure/external/, so use-cases do not import the SDK. - Templates — verification, revalidation; rendered server-side or linked from the frontend.
-
Tokens — the schema already has
ExchangeRequest.hash; use an unguessable token with an expiry, and store a hash — not the raw token. - Triggers — on exchange creation/revalidation (and any later claim/accept flow).
- Failure handling — a failed send must be visible (log/metric) and retryable; do not silently swallow it like the legacy code.
-
Tests — a fake
EmailSenderin route tests, asserting the use-case called it; no real network.
- 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.
- 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.
- Exchange — the current legacy email-link flow
- Planned endpoints — exchange/verification endpoints
- Decision records — Resend is record 0016, status Proposed
-
Domain model —
ExchangeRequest.hash - Auth & sessions — the separate login flow (no email)
Last verified: 2026-11-02