Plan: Multi-brand email platform (Listmonk + Resend) for Nova, Sip, Gus, JustinWieb¶
URL: https://mkdocs.justinsforge.com/memory/plans/multi-brand-email-listmonk-resend-2026-07-01/
SUPERSEDED 2026-07-08. Listmonk was retired in favor of Mautic and fully removed from Forge (see reference_forge_mail_mautic). The authoring layer is Mautic; Resend remains the delivery engine. This doc is kept as a historical record only, read "Listmonk" as "Mautic" for anything still relevant.
Date: 2026-07-01
Approved spec: Self-hosted email marketing platform Justin owns. Listmonk on forge is the campaign/newsletter authoring layer for all 4 businesses (Nova Design, Sip N Serve, Gus Outdoor Co, JustinWieb), each with its own list, from-address, and template. Resend is the invisible SMTP delivery engine behind Listmonk (DKIM-signed per brand domain) and also the API for transactional/triggered mail (React Email). Admin UI at lists.justinsforge.com behind Cloudflare Access. Krystal gets a Mailchimp-like newsletter experience plus a Gmail Send-As path for 1:1 as [email protected]. Retire Mailchimp ($28/mo) after Sip list migrates.
Out of scope this round:
- Subscriber list imports (Sip Mailchimp CSV, Gus contacts) — build empty lists now, import when data is provided
- AI-assisted campaign drafting into Listmonk
- Advanced automations / drip sequences
- Actually cancelling Mailchimp (keep until Sip verified migrated)
- Nova growth list (winding down; provision list only)
State already done (2026-07-01):
- Resend account live (owner [email protected]), full-access key in ~/.forge-secrets/resend.env
- sipnservesociety.com registered in Resend (id dfef2247-fec8-403a-a4d9-f737a5210874); DKIM+MAIL FROM DNS written to Cloudflare; verification pending
- SES sandbox reopen case still pending (fallback path only)
Task list¶
Task 1: Dedicated Resend sending key + secret hygiene¶
- Files:
~/.forge-secrets/resend.env(edit; addRESEND_SMTP_KEY), delete exposed sending-only key in Resend dashboard (manual) - What: Create a sending-only Resend key for SMTP/Listmonk + Gmail Send-As use (keep full-access key for management only). Store as
RESEND_SMTP_KEY. Delete the original sending key that was pasted in chat. - Verification:
set -a; source ~/.forge-secrets/resend.env; curl -s -o /dev/null -w '%{http_code}\n' https://api.resend.com/emails -H "Authorization: Bearer $RESEND_SMTP_KEY" -X POST -H 'content-type: application/json' -d '{"from":"[email protected]","to":["[email protected]"],"subject":"key test","html":"ok"}'returns200 - Commit:
chore(email): dedicated resend sending key, rotate exposed key
Task 2: Register remaining 3 brand domains in Resend¶
- Files: none (API calls); record ids to
memory/general/reference_resend_email_stack.mdin final task - What: POST
/domainsforgusoutdoor.co,justinwieb.com,shopnovadesign.comusing full-access key; capture DKIM + MAIL FROM records for each. - Verification:
curl -s https://api.resend.com/domains -H "Authorization: Bearer $RESEND_API_KEY" | jq '.data[].name'lists all 4 domains - Commit:
feat(email): register gus/justinwieb/nova domains in resend
Task 3: Write Resend DNS records to Cloudflare for the 3 domains¶
- Files: none (Cloudflare API via
~/.forge-secrets/cloudflare.env) - What: For each of the 3 domains, add the
resend._domainkeyTXT,sendMX, andsendTXT (SPF) records idempotently. Check each root SPF for collision first (Resend SPF is on thesendsubdomain so should not collide; abort a domain if it would). - Verification:
for d in gusoutdoor.co justinwieb.com shopnovadesign.com; do echo $d; dig +short TXT resend._domainkey.$d; dig +short MX send.$d; doneshows records for all 3 - Commit:
feat(dns): resend dkim/mailfrom records for 3 brand domains
Task 4: Trigger + confirm verification for all 4 domains¶
- Files: none
- What: POST
/domains/{id}/verifyfor all 4; poll status untilverified. (DNS propagation may take up to ~1h.) - Verification:
curl -s https://api.resend.com/domains -H "Authorization: Bearer $RESEND_API_KEY" | jq '.data[] | {name, status}'shows all 4verified - Commit:
chore(email): all 4 brand domains verified in resend
Task 5: Listmonk compose + config in repo¶
- Files:
/home/justinwieb/forge/infra/listmonk/compose.yml,/home/justinwieb/forge/infra/listmonk/config.toml,/home/justinwieb/forge/infra/listmonk/.env.example - What: Docker Compose stack:
listmonk/listmonkapp + dedicatedpostgres:17(own volume, not the lifeos spine). Config points admin at127.0.0.1:9000; secrets (DB pass, Listmonk admin pass) referenced from env, real values in~/.forge-secrets/listmonk.env. - Verification:
docker compose -f /home/justinwieb/forge/infra/listmonk/compose.yml configparses with no error - Commit:
feat(listmonk): compose + config for self-hosted stack
Task 6: Deploy Listmonk on the forge Docker host¶
- Files:
~/.forge-secrets/listmonk.env(create, chmod 600) - What: Pick host (confirm capacity on media-server CT 102 Docker host, else provision a small LXC on Finn), run DB install (
--install), bring the stack up. - Verification:
curl -fsS http://127.0.0.1:9000/api/healthreturnstrue(from the host) - Commit:
feat(listmonk): deploy stack on forge docker host
Task 7: Cloudflare tunnel + Access for lists.justinsforge.com¶
- Files: tunnel ingress config (via
/cf-addskill pattern), Cloudflare Access policy (email allowlist: Justin + Krystal) - What: Expose Listmonk at
lists.justinsforge.comthrough the tunnel; put Cloudflare Access in front (per Security Rule #4, sensitive service needs auth). - Verification:
curl -sI https://lists.justinsforge.com | grep -i cf-accessshows Access challenge (302 to login), and authenticated browser reaches the Listmonk login - Commit:
feat(listmonk): cloudflare tunnel + access at lists.justinsforge.com
Task 8: Wire Listmonk SMTP to Resend¶
- Files: Listmonk Settings (DB-stored, via admin UI or
config.tomlSMTP block) - What: Configure Listmonk SMTP: host
smtp.resend.com, port587, STARTTLS, usernameresend, passwordRESEND_SMTP_KEY. Set a low concurrency + message rate to stay clean. - Verification: Listmonk admin "Send test email" to
[email protected]succeeds; confirm inbox landing via Gmail OAuth search - Commit:
feat(listmonk): smtp delivery via resend
Task 9: Create 4 brand lists + from-identities¶
- Files: Listmonk data (via admin API)
- What: Create one public opt-in list per brand (Nova, Sip, Gus, JustinWieb), each with its brand from-name and from-address on its verified domain (e.g.
[email protected],[email protected], etc.). All empty. - Verification:
curl -s -u api-user:token http://127.0.0.1:9000/api/lists | jq '.data.results[].name'shows all 4 lists - Commit:
feat(listmonk): 4 brand lists + from-addresses
Task 10: Base brand campaign template¶
- Files:
/home/justinwieb/forge/infra/listmonk/templates/base-brand.html(source of truth), loaded into Listmonk - What: One clean, reusable responsive template with brand logo/color slots + a required unsubscribe footer, so Krystal just drops in content. Version-controlled in repo, imported to Listmonk.
- Verification: Template renders in Listmonk preview without unfilled-variable errors; contains
{{ UnsubscribeURL }} - Commit:
feat(listmonk): reusable base brand template
Task 11: End-to-end test campaign (Sip)¶
- Files: none
- What: Add Justin's addresses to a temp test list, send a real Sip N Serve campaign from
[email protected]through the full Listmonk→Resend chain. - Verification: Gmail OAuth search confirms the campaign landed in
INBOX(not spam), DKIMpass; Listmonk shows 1 sent, 0 bounced - Commit:
test(listmonk): sip end-to-end campaign verified inbox
Task 12: Resend transactional helper (forge_resend.py)¶
- Files:
/home/justinwieb/forge/scripts/forge_resend.py - What: Thin, reusable Resend API wrapper (
send(from_addr, to, subject, html, tags)), readsRESEND_API_KEY, fails loud on non-2xx, idempotency-key support. This is the transactional lane sender. - Verification:
python3 /home/justinwieb/forge/scripts/forge_resend.py --smokesends a test to[email protected]and prints the message id - Commit:
feat(email): forge_resend.py transactional helper
Task 13: React Email project + first transactional template¶
- Files:
/home/justinwieb/forge/infra/react-email/(scaffold),.../emails/sip-signup-confirmation.tsx - What: Scaffold a React Email project; build the Sip signup-confirmation template; add a render script that outputs HTML for
forge_resend.pyto send. (Trigger wiring to real signup events is a later round.) - Verification:
cd /home/justinwieb/forge/infra/react-email && npm run render sip-signup-confirmationoutputs valid HTML to stdout - Commit:
feat(email): react-email project + sip signup template
Task 14: Gmail Send-As for Krystal ([email protected] via Resend)¶
- Files: none (Gmail settings on Krystal's personal Gmail; needs the confirmation code at
hello@) - What: Confirm
[email protected]is a reachable Workspace mailbox/alias; configure Gmail "Send mail as" with Resend SMTP (RESEND_SMTP_KEY); retrieve the Gmail confirmation code fromhello@. - Verification: Krystal sends a test from her Gmail choosing
[email protected]; Gmail OAuth confirms it sent and the From header is[email protected] - Commit:
feat(email): krystal gmail send-as via resend smtp(docs only; no repo code)
Task 15: Home landing tile (if applicable)¶
- Files: justinsforge.com home landing source (per
feedback_new_subdomain_links_home.md) - What: Evaluate whether
lists.justinsforge.com(internal admin behind Access) warrants a home tile. If yes per doctrine, add it; if exempt as an internal/3rd-party UI, note the exemption. Default: exempt, document the decision. - Verification: Decision recorded in the reference topic file; if a tile is added,
curl -s https://justinsforge.com | grep -i listsfinds it - Commit:
docs(home): listmonk subdomain home-tile decision
Task 16: Register and document¶
- Files:
MEMORY.md(index line),memory/general/reference_resend_email_stack.md(new),memory/general/reference_listmonk_platform.md(new) - What: Document the full stack: Resend account + domain ids + keys location, Listmonk host/URL/admin, the two lanes (Listmonk campaigns vs React Email transactional), Gmail Send-As, and the Mailchimp-retirement checklist. Add MEMORY.md index lines. Run eval harness.
- Verification:
bash /home/justinwieb/forge/scripts/forge_eval_run.shpasses;grep -l resend_email_stack memory/general/andgrep listmonk MEMORY.mdboth hit - Commit:
docs(email): register resend+listmonk stack in memory