Internal Documentation internal
TalkIDE internal documentation

Status: Accepted Datum: 2026-05-16 Oblast: Infrastruktura / E-mail / Notifikace Supersedes: — Navazuje na: UC-01005 (Forgotten Password), UC-11001 (Join Waitlist)

Context

Stávající stav — tři e-mailové stuby

TalkIDE má tři místa, kde uživatel přirozeně očekává e-mail, ale žádný se neposílá:

StubUCStav
Waitlist confirmationUC-11001UI zobrazuje “We sent a confirmation to {email}”, žádný e-mail neodchází
Forgot-password reset linkUC-01005MVP: reset link se loguje pouze do konzoly BE
Billing spending alert (80 % / 100 %)UC-10005MVP: Slf4j log only

Billing alert je mimo scope tohoto ADR — řeší se samostatně v rámci Stopy C (real Stripe live mode). Aktuálně zůstává jako stub.

Požadavky

  • Přejít z MVP no-op / log-only na reálné odeslání pro forgot-password a waitlist confirmation.
  • Abstrakce umožňující snadné testování (dev/test profil odesílá do /dev/null, nevolá žádné API).
  • Auditní log odeslaných e-mailů v DB (pro debugging doručitelnosti a budoucí billing usage tracking).
  • Sending doména na subdoméně mail.talkide.app — neovlivňuje MX záznamy hlavní domény.
  • Konfigurovatelný API base URL (US vs EU region).

Decision

1. Provider — Mailgun

Zvolena platforma Mailgun (Sinch). Důvody:

  • Existující firemní účet — nulový onboarding čas.
  • Robustní HTTP API (https://api.mailgun.net/v3/{domain}/messages) s formátem multipart/form-data.
  • Webhook events (delivered, failed, bounced) pro budoucí doručitelnostní monitoring.
  • Alternativy zvažovány, ale odmítnuty (viz sekce Alternatives).

2. Transport — HTTP API, nikoli SMTP

Integrace probíhá výhradně přes Mailgun HTTP API. Závislost spring-boot-starter-mail (JavaMail/SMTP) se nepřidává. Důvody:

  • HTTP API je synchronní, snadněji testovatelné a nevyžaduje správu SMTP connection pool.
  • Jednodušší retry logika (HTTP status kódy vs. SMTP error codes).
  • Přímočará autentizace (Authorization: Basic api:<key>).

3. Sending doména — mail.talkide.app

E-maily odcházejí z adresy noreply@mail.talkide.app, zobrazovaný název odesílatele je TalkIDE. Používá se subdoména, aby:

  • DNS záznamy pro e-mail (SPF, DKIM, tracking CNAME) neovlivňovaly hlavní doménu talkide.app.
  • Mailgun verifikace domény nezasahovala do stávajícího DNS setupu.

DNS prerequisite (manuální konfigurace Mailgunu před prvním prod odesláním):

Typ záznamuHodnota
TXT (SPF)v=spf1 include:mailgun.org ~all na mail.talkide.app
TXT (DKIM)k=rsa; p=<public_key> na pic._domainkey.mail.talkide.app
CNAME (tracking)mailgun.org na email.mail.talkide.app
MX (volitelné)pouze pokud chceme příjmové e-maily na subdoméně

DNS záznamy jsou generovány Mailgunem po přidání domény do panelu. Až do jejich ověření Mailgunem prod odesílání nefunguje.

4. Konfigurovatelný API base URL

# application.yaml
talkide:
  email:
    provider: mailgun
    mailgun:
      base-url: https://api.mailgun.net   # US region; EU: https://api.eu.mailgun.net
      domain: mail.talkide.app
      from-name: TalkIDE
      from-address: noreply@mail.talkide.app

base-url je konfigurovatelný přes env, bez nutnosti kódu měnit region.

5. EmailSender abstrakce

interface EmailSender {
    fun send(to: String, subject: String, htmlBody: String, textBody: String): EmailSendResult
}

Dvě implementace:

ImplementaceProfilChování
MailgunEmailSenderproductionSkutečné HTTP volání Mailgun API; loguje provider_message_id z odpovědi
NoOpEmailSender!production (všechny ne-production profily — local, test, default, dev, …)Žádné HTTP volání; loguje kompletní e-mail (headers + body) do konzoly na úrovni INFO

Konzumenti (use cases) pracují pouze s rozhraním EmailSender — neví, která implementace je aktivní.

6. Audit log — tabulka email_log

Nová Liquibase migrace 0028-create-email-log.xml:

CREATE TABLE email_log (
  id                  BIGSERIAL PRIMARY KEY,
  type                VARCHAR(50)  NOT NULL,       -- FORGOT_PASSWORD | WAITLIST_CONFIRMATION | BILLING_ALERT
  recipient           VARCHAR(255) NOT NULL,
  subject             VARCHAR(500) NOT NULL,
  provider_message_id VARCHAR(255),                -- Mailgun-Message-Id z response header; null u NoOp
  status              VARCHAR(20)  NOT NULL,        -- SENT | FAILED
  error               TEXT,                         -- null pokud SENT; chybová zpráva pokud FAILED
  created_at          TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- Composite index optimalizovaný pro throttle lookup (najdi poslední SENT WAITLIST_CONFIRMATION
-- pro daného příjemce): filtruje po recipient + type, řadí/limituje dle created_at.
CREATE INDEX idx_email_log_recipient_type_created ON email_log (recipient, type, created_at);

Záznam se persistuje vždy — i při selhání odeslání (status=FAILED, error=…). Tabulka slouží pro debugging doručitelnosti a budoucí compliance.

7. Secrets pattern — K8s Secret mailgun-creds

# kubectl create secret generic mailgun-creds -n talkide \
#   --from-literal=api-key=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

BE pod injectuje secret jako env proměnnou:

env:
  - name: TALKIDE_EMAIL_MAILGUN_API_KEY
    valueFrom:
      secretKeyRef:
        name: mailgun-creds
        key: api-key

Secret se spravuje ručně (stejný vzor jako db-creds a anthropic-creds). Secret template není součástí Helm chartu.

8. Chování pro jednotlivé stuby

UCTriggerTemplateChování při selhání
UC-01005BE generuje reset tokenSubject: “Reset your TalkIDE password”, text s linkemSelhání loge, ignoruje — response je vždy 200 OK (anti-enumeration)
UC-11001Po 201 vždy; po idempotentním 200 jen pokud email_log neobsahuje úspěšně odeslaný WAITLIST_CONFIRMATION pro tohoto příjemce za posledních WAITLIST_CONFIRMATION_RESEND_WINDOW_HOURS = 24 h (throttled resend)Subject: “You’re on the TalkIDE waitlist!”, text s potvrzením + referral linkSelhání loge, ignoruje — join request NESMÍ selhat kvůli e-mailu
UC-10005 (billing alert)Spending limit 80 %/100 %MIMO SCOPE tohoto ADRZůstává Slf4j log stub

Consequences

Pozitivní

  • Waitlist uživatelé obdrží reálné potvrzení e-mailem — eliminuje nesoulad UI copy vs. realita.
  • Forgot-password flow je kompletní end-to-end bez manuálního vyhledávání v BE logu.
  • Auditní tabulka email_log umožňuje ověřit doručení bez přístupu do Mailgun dashboardu.
  • NoOpEmailSender zajišťuje, že ve všech ne-production prostředích (!production) neodcházejí reálné e-maily; reset link je stále viditelný v konzole.
  • Abstrakce EmailSender umožňuje v budoucnu vyměnit Mailgun za jiný provider bez změny business logiky.

Negativní / Rizika

  • DNS prerequisite: prod odesílání nefunguje, dokud nejsou ověřeny DNS záznamy Mailgunu pro mail.talkide.app. Toto je manuální krok mimo kód.
  • Nový K8s Secret mailgun-creds musí být vytvořen před deploym na prod; absence způsobí chybu startupového ENV injection (pod nepůjde nastartovat pokud je required: true).
  • Billing alert zůstává stub — přidáváme email_log tabulku, ale BILLING_ALERT type se zatím nezapisuje (připraveno pro budoucí implementaci).
  • Bez retry logiky v MVP — pokud Mailgun API vrátí 5xx, MailgunEmailSender zaloguje chybu a zapíše FAILED do email_log. Žádný exponential backoff / dead-letter queue. Toto je akceptovatelné pro aktuální objem.
  • Profil MUSÍ být přesně production — chybný název profilu (např. prod) způsobí tichý fallback na NoOpEmailSender a žádné e-maily se neposílají. Tato chyba nastala v produkci a byla opravena v commitu 13002d2.

Alternatives Considered

ProviderDůvod odmítnutí
ResendŽádný existující účet; modernější DX, ale zbytečný onboarding overhead
PostmarkSilný v transakčních e-mailech, dobrá reputace, ale opět nový účet + vyšší cena pro nízký objem
AWS SESVýhodný pro vysoký objem, ale komplikovanější setup (IAM, sandbox vyjímka) a přidaná DO↔AWS závislost
SMTP (spring-boot-starter-mail)Přidává JavaMail stack; SMTP session management je zbytečně složitý pro nízký objem; horší testovatelnost
SendGridVlastněno Twiliem; cena a složitost API v porovnání s Mailgunem bez výhod pro náš use case

Prerequisites Checklist (před prod deploym)

  • Mailgun doména mail.talkide.app přidána a ověřena v Mailgun panelu (DNS záznamy SPF, DKIM, tracking CNAME propagovány)
  • K8s Secret mailgun-creds vytvořen v namespace talkide s klíčem api-key
  • BE Helm chart / deployment YAML aktualizován o env injection TALKIDE_EMAIL_MAILGUN_API_KEY
  • Liquibase migrace 0028-create-email-log.xml aplikována (stane se automaticky při prvním startu BE)
  • Smoke test: ruční POST /api/v1/auth/forgot-password na prod → ověřit příchod e-mailu a záznam v email_log

Was this page helpful?

Thanks for the feedback.