Una newsletter sense proveïdor: DynamoDB, SES i 150 línies
Per què no Mailchimp
Mailchimp és una bona eina. Però té un preu: a partir d'uns quants centenars de contactes, pagues cada mes tant si envies com si no. I cedeixes les dades dels teus subscriptors a un tercer.
Per a una llista petita, on l'únic que necessites és guardar adreces, enviar un correu i permetre la baixa, la plataforma és excés. Pots construir el que necessites amb el que ja tens a AWS.
Això és el que he fet a joan-utils, el meu stack de CloudFormation a eu-west-3.
Què he construït de debò
Una taula DynamoDB en mode PAY_PER_REQUEST. La clau de partició és email. Cada ítem té un camp token (UUID v4), un camp status i un createdAt.
Els estats possibles són tres: pending, active i unsubscribed. La transició és sempre cap endavant: mai es torna a pending des d'active.
Hi ha un índex secundari global (token-index) sobre el camp token. Sense aquest índex, confirmar o donar de baixa una adreça requeriria un scan de tota la taula. Amb l'índex, és una consulta puntual.
Una sola Lambda en Node 24, arquitectura arm64, memòria 512 MB, darrere d'un HTTP API de API Gateway. Tres rutes:
POST /newsletter/subscribeGET /newsletter/confirmGET /newsletter/unsubscribe
La doble confirmació no és opcional
Si acceptes una adreça sense confirmar, qualsevol pot subscriure qualsevol altra persona. Rebràs rebots. SES et penalitza pels rebots: si superes un cert percentatge, et suspenen l'enviament.
Amb la doble confirmació, l'adreça no entra a la llista fins que el propietari fa clic. Punt.
El flux és: reps el POST, guardes l'ítem en estat pending, envies el correu de confirmació amb l'enllaç GET /newsletter/confirm?token=UUID. Quan l'usuari fa clic, passes l'ítem a active.
El token de baixa a cada correu
Cada correu d'enviament ha de portar un enllaç de baixa. No a la pàgina principal, no a un formulari: un enllaç directe que faci la baixa en un sol clic.
El token que guardes a DynamoDB serveix per a les dues coses: confirmació i baixa. No cal generar-ne un de nou per a cada enviament.
El que no pots fer és respondre 404 si l'adreça ja és unsubscribed. Respondries informació sobre l'estat de la llista. Retorna sempre 200.
El cas de l'adreça ja activa
Si algú envia el formulari dues vegades amb la mateixa adreça i l'estat ja és active, no reenvies el correu de confirmació. Retorna 200 i prou. Si reenviessis, estaríes confirmant implícitament que aquella adreça és a la llista. No filtris informació.
Els números de la primera prova real
Aquestes són les mesures que he obtingut:
- Alta (cold start): 518 ms
- Confirmació (segona invocació): 149 ms
- Tercera invocació (warm): 6,5 ms
El cold start de 518 ms és acceptable per a un formulari web. No és un camí crític.
El cost del tram:
- SES: 0,10 $ per cada 1.000 correus
- DynamoDB: sota el nivell gratuït amb qualsevol llista petita
- Lambda: dècimes de cèntim al mes
Per a una llista de menys de deu mil subscriptors, el cost mensual és essencialment zero si no envies cada dia.
Quan NO compensa fer-t'ho tu
Aquesta solució cobreix: guardar adreces, confirmar, donar de baixa, enviar un correu simple.
No cobreix:
- Plantilles visuals: SES accepta HTML, però el disseny i el manteniment de les plantilles van a càrrec teu.
- Segmentació: si vols enviar coses diferents a grups diferents, necessites més camps, més índexs i més lògica.
- Informes d'obertura i clics: SES pot publicar events a SNS, però processar-los i mostrar-los és un projecte a part.
- Gestió de rebots i queixes automàtica: SES envia notificacions, però has d'implementar el handler que actualitzi els estats.
Si necessites alguna d'aquestes coses, una plataforma dedicada et sortirà més barata en hores de feina. La decisió no és ideològica: és quant val el teu temps.
El que ha costat de trobar
Dos problemes que no eren evidents:
Primer, l'índex secundari global triga uns segons a estar disponible després de crear la taula. En un desplegament de CloudFormation en un entorn nou, la primera invocació pot fallar si la Lambda s'executa just després de la creació. Ho vaig resoldre afegint un DependsOn explícit al recurs de la Lambda cap a la taula, i acceptant que el primer cold start pot trobar l'índex encara en estat CREATING. En producció no és un problema perquè la taula ja existeix.
Segon, SES a eu-west-3 requereix que el domini estigui verificat. Si estàs en sandbox, a més, les adreces de destinació també han d'estar verificades. Vaig perdre una estona fins que vaig entendre que el problema no era el codi sinó que SES encara no havia sortit del sandbox.
T'aviso quan publiqui
Un correu per setmana com a molt, i només quan publiqui alguna cosa. De tant en tant t'hi explico el que faig: cursos, sessions, producte. Te'n pots donar de baixa quan vulguis, amb un clic.