CreatorTrack Campaigns app (Mautic + Resend, read-only v1)¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_creatortrack_campaigns_app/
Cross-brand email marketing cockpit inside CreatorTrack: per-brand subscriber counts, campaign list, open/click/fail rates, per-email reports. Read-only; authoring/sending deep-links to mautic.justinsforge.com. Built 2026-07-06 on branch campaigns (plan: creatortrack-campaigns-app-2026-07-06).
LIVE on prod 2026-07-06 (merge b280b44). Deploy note: the branch predated the finance proxy pattern, so the integrator had to add /api/campaigns/jobs to PUBLIC_PATHS in proxy.ts (cookie-exempt, still bearer-gated) or the middleware 401s the timer POSTs before the route runs (commit b280b44). Timer installed user scope (~/.config/systemd/user/, mirrors finance timers), NOT /etc/systemd/system. Both jobs verified green against live Mautic/Resend on prod (11 emails, 3 segments, 56 resend events).
Architecture (finance-module pattern)¶
- PG read copy
campaigns.*in lifeos:emails,segments,send_events,job_runs. All workspace-scoped + RLSworkspace_isolation, grants tolifeos_app. Migrationdb/migrations/20260706T145955_campaigns_campaigns_schema.sql(suite repo). - Brand map is a TS constant, NOT a table:
lib/campaigns/brands.ts(6 brands, Mautic category ids: gusoutdoor 1 / nova 2 / gusbass 3 / justinwieb 4; sipnserve + creatortrack have none). Attribution rule: category id first, from-address domain fallback, name hints last. NEVER scope by Mautic segment visibility (segments are account-wide). - Ingest jobs
lib/campaigns-jobs.ts+lib/campaigns-jobs/{mautic-sync,resend-poll}.ts. mautic-sync: emails + segments (contact counts via/api/contacts?search=segment:<alias>) + 90demail_statsrows -> send_events (sent/read/failed); prunes rows deleted in Mautic; recipient addresses stored only as sha256 hashes. resend-poll: newest 100 Resend sends -> send_events (delivered/bounced/...). Both idempotent (upsert keys), log tocampaigns.job_runs, fail loud via forge_notify. - Clients:
lib/campaigns/mautic-read.ts(Basic auth vs LANhttp://192.168.86.87:8090, needsX-Forwarded-Proto: httpsheader or Mautic 301s),lib/campaigns/resend-read.ts(RESEND_API_KEYviaresendEnv()). - Route: POST
/api/campaigns/jobs/[job], bearerCAMPAIGNS_JOBS_TOKEN(in~/.forge-secrets/creatortrack-auth.env, timingSafeEqual), jobs:noop,mautic-sync,resend-poll. - UI: app id
campaignsinlib/apps.ts(route-app, ownerOnly, "New email"). Pagesapp/(suite)/campaigns/page.tsx(all-brands grid;?brand=<key>detail: tiles + 30d SVG send chart + emails + segments) andapp/(suite)/campaigns/email/[id]/page.tsx(per-email report). CSScomponents/apps/campaigns/campaigns.css. - Read layer:
lib/campaigns.ts(rollups, series, email report); RLS scopes reads, no explicit workspace filter. - Timers (forge repo):
scripts/forge_creatortrack_campaigns_jobs.sh(env overrideCAMPAIGNS_JOBS_BASEfor dev slots) +scripts/systemd/forge-creatortrack-campaigns-sync.{service,timer}(every 30 min). NOT enabled until prod deploy.
Deploy checklist (post-merge)¶
- Merge branch
campaignsinto suite main (single AI-reconciled merge, WDA doctrine). - Apply migration to live lifeos:
cd ~/creatortrack && WORKSPACE_MIGRATIONS_DIR=db/migrations python3 ~/forge/scripts/forge_workspace_migrate.py. - Ensure
CAMPAIGNS_JOBS_TOKENreaches creatortrack-prod's environment (already increatortrack-auth.env); restartcreatortrack-prod.service. systemctl enable --now forge-creatortrack-campaigns-sync.timer(symlink units from~/forge/scripts/systemd/).- Teardown dev slot:
git -C ~/forge-suite worktree remove ~/forge-suite-wt/campaigns+DROP DATABASE ct_campaigns.
Verified 2026-07-06 (dev slot :3063, DB ct_campaigns)¶
Both jobs green end-to-end against live Mautic/Resend (11 emails, 3 segments, 18 stat rows, 56 resend events; all 6 brands attribute correctly). Route auth 200/401/404 correct. Dashboard, brand view, and email report render with real data. Trigger script dry-run green.
Related: reference_forge_mail_mautic, reference_creatortrack_finance_module, reference_creatortrack_parallel_dev.
[Claude Code]