Skip to content

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; add RESEND_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"}' returns 200
  • 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.md in final task
  • What: POST /domains for gusoutdoor.co, justinwieb.com, shopnovadesign.com using 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._domainkey TXT, send MX, and send TXT (SPF) records idempotently. Check each root SPF for collision first (Resend SPF is on the send subdomain 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; done shows 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}/verify for all 4; poll status until verified. (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 4 verified
  • 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/listmonk app + dedicated postgres:17 (own volume, not the lifeos spine). Config points admin at 127.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 config parses 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/health returns true (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-add skill pattern), Cloudflare Access policy (email allowlist: Justin + Krystal)
  • What: Expose Listmonk at lists.justinsforge.com through 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-access shows 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.toml SMTP block)
  • What: Configure Listmonk SMTP: host smtp.resend.com, port 587, STARTTLS, username resend, password RESEND_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), DKIM pass; 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)), reads RESEND_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 --smoke sends 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.py to send. (Trigger wiring to real signup events is a later round.)
  • Verification: cd /home/justinwieb/forge/infra/react-email && npm run render sip-signup-confirmation outputs 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 from hello@.
  • 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 lists finds 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.sh passes; grep -l resend_email_stack memory/general/ and grep listmonk MEMORY.md both hit
  • Commit: docs(email): register resend+listmonk stack in memory