Plan: CreatorTrack "Campaigns" app (Mautic + Resend, read-only v1)¶
URL: https://mkdocs.justinsforge.com/memory/plans/creatortrack-campaigns-app-2026-07-06/
Date: 2026-07-06
Approved spec: New global "Campaigns" route-app in CreatorTrack with a brand switcher across all 6 Resend-verified brands (Sip N Serve, CreatorTrack, Gus Outdoor, Gus The Bass, Nova, JustinWieb). Read-only surfaces: per-brand dashboard (segment counts, sends over time, open/click/fail rates), campaign/email list, per-email report. Write-shaped actions deep-link to mautic.justinsforge.com. Data layer clones the finance module pattern: campaigns.* PG schema as read copy, TS ingest jobs pulling Mautic REST (LAN http://192.168.86.87:8090/api) and polling the Resend API, token-authed /api/campaigns/jobs/[job] route, systemd timers forge-creatortrack-campaigns-*. Brand mapping keys off Mautic category + from-address domain (never segment visibility). Idempotent upserts on Mautic/Resend IDs; job failures logged to campaigns.job_runs and fail loud.
Out of scope: email builder, send/schedule actions, contact editing, Resend webhooks, list-growth tooling, non-owner visibility.
Dev rig: branch campaigns, worktree ~/forge-suite-wt/campaigns/, DB ct_campaigns (TEMPLATE clone of creatortrack_dev_base), dev server next dev --webpack on :3063 (preview http://100.97.43.104:3063; :3061/:3062 held by existing slots). Bootstrap: bash ~/forge/scripts/forge_creatortrack_agent_bootstrap.sh campaigns 3063.
Task list¶
Task 1: Create campaigns schema migration + brand constant¶
- Files:
~/forge-suite-wt/campaigns/db/migrations/<stamp>_campaigns_campaigns_schema.sql(create withforge_workspace_new_migration.pyfrom the worktree),~/forge-suite-wt/campaigns/lib/campaigns/brands.ts - What: Idempotent DDL for schema
campaigns, all tables workspace-scoped with the finance-styleworkspace_isolationRLS policy +lifeos_appgrants:emails(workspace_id, mautic_id, brand_key, name, subject, from_address, email_type, is_published, publish_up, date_added, sent_count, read_count, failed_count, synced_at, UNIQUE(workspace_id, mautic_id)),segments(workspace_id, mautic_id, brand_key, name, alias, contact_count, synced_at, UNIQUE(workspace_id, mautic_id)),send_events(workspace_id, source in ('mautic','resend'), source_id, email_mautic_id, brand_key, recipient_hash, event, event_at, UNIQUE(workspace_id, source, source_id, event)),job_runs(mirror of finance.job_runs). Brand map is NOT a table (single source of truth):lib/campaigns/brands.tsexports the 6-brand constant (key, name, mauticCategoryId: gusoutdoor 1/nova 2/gusbass 3/justinwieb 4/sipnserve null/creatortrack null, fromDomain, mauticBaseUrl deep-link helper). - Verification:
cd ~/forge-suite-wt/campaigns && PGDATABASE=ct_campaigns WORKSPACE_MIGRATIONS_DIR=db/migrations python3 ~/forge/scripts/forge_workspace_migrate.pythen psql\dt campaigns.*shows 4 tables. - Commit:
feat(campaigns): campaigns schema + brand map
Task 2: Mautic read client¶
- Files:
~/forge-suite-wt/campaigns/lib/campaigns/mautic-read.ts - What: Server-only read client reusing
mauticEnv()creds fromlib/mautic.ts(do not touch the signup-sync code). Functions:listMauticEmails()(GET /api/emails, paginated; includes sentCount/readCount/category/fromAddress),listMauticSegments()(GET /api/segments),fetchEmailStatRows(since)(GET /api/stats/email_stats windowed). Brand resolution helper: category id first, from-address domain fallback. - Verification:
cd ~/forge-suite-wt/campaigns && npx tsx -e "import('./lib/campaigns/mautic-read.ts').then(async m => console.log((await m.listMauticEmails()).length))"prints >= 8. - Commit:
feat(campaigns): mautic read client
Task 3: Resend read client¶
- Files:
~/forge-suite-wt/campaigns/lib/campaigns/resend-read.ts - What: Server-only client using
RESEND_API_KEYvialib/secrets.ts(resend.env). Functions:listResendDomains()(GET /domains),listRecentResendEmails()(GET /emails, paginated window). Map each email to brand by from-address domain. - Verification:
npx tsx -e "import('./lib/campaigns/resend-read.ts').then(async m => console.log((await m.listResendDomains()).map(d => d.name)))"prints 6 domains. - Commit:
feat(campaigns): resend read client
Task 4: Ingest jobs (mautic-sync, resend-poll)¶
- Files:
~/forge-suite-wt/campaigns/lib/campaigns-jobs.ts(dispatcher:isKnownJob/runJob, mirroringlib/finance-jobs.ts),~/forge-suite-wt/campaigns/lib/campaigns-jobs/mautic-sync.ts,~/forge-suite-wt/campaigns/lib/campaigns-jobs/resend-poll.ts - What:
mautic-syncupserts brands' emails/segments/stat rows intocampaigns.*;resend-pollupserts delivery/bounce events intocampaigns.send_events. Upsert keys = Mautic/Resend IDs (safe to run twice). Every run logged tocampaigns.job_runs; errors re-raise after logging. - Verification:
npx tsx -e "import('./lib/campaigns-jobs.ts').then(m => m.runJob('mautic-sync'))"thenpsql -c "select count(*) from campaigns.emails"> 0 andselect * from campaigns.job_runs order by id desc limit 2shows ok=true. - Commit:
feat(campaigns): mautic-sync + resend-poll ingest jobs
Task 5: Token-authed jobs route¶
- Files:
~/forge-suite-wt/campaigns/app/api/campaigns/jobs/[job]/route.ts; addCAMPAIGNS_JOBS_TOKENto~/.forge-secrets/creatortrack-auth.env - What: POST route,
dynamic="force-dynamic", bearer auth viatimingSafeEqualagainstCAMPAIGNS_JOBS_TOKEN(same shape asapp/api/finance/jobs/[job]/route.ts), dispatches torunJob, 404 on unknown job. - Verification:
curl -fsS -X POST -H "Authorization: Bearer $CAMPAIGNS_JOBS_TOKEN" http://127.0.0.1:3061/api/campaigns/jobs/mautic-syncreturns{"ok":true,...}; wrong token returns 401. - Commit:
feat(campaigns): token-authed jobs route
Task 6: Read-layer queries¶
- Files:
~/forge-suite-wt/campaigns/lib/campaigns.ts - What: Server queries for the UI:
listBrands()(with per-brand rollups),brandOverview(brandKey)(segment counts, 30/90-day send + open/click/fail series from send_events + email stats),listCampaignEmails(brandKey),emailReport(mauticId). Pure reads vialib/db.ts. - Verification:
npx tsx -e "import('./lib/campaigns.ts').then(async m => console.log(await m.listBrands()))"prints 6 brands with counts. - Commit:
feat(campaigns): read-layer queries
Task 7: Register app + dashboard page¶
- Files:
~/forge-suite-wt/campaigns/lib/apps.ts,~/forge-suite-wt/campaigns/app/campaigns/page.tsx, shared UI components under~/forge-suite-wt/campaigns/components/campaigns/ - What: Add
{ id: "campaigns", name: "Campaigns", route: "/campaigns", ownerOnly: true, newLabel: true }route-app (News-hub pattern). Dashboard page: brand switcher (all 6), per-brand stat tiles (subscribers, sends, open/click/fail rates), sends-over-time chart, recent campaigns list. "Open in Mautic" + "New email" deep links tohttps://mautic.justinsforge.com/s/emails. - Verification: dev server on :3061,
curl -fsS http://127.0.0.1:3061/campaigns | grep -c Campaigns>= 1; visual check via agent-browser (npx agent-browser open http://127.0.0.1:3061/campaigns). - Commit:
feat(campaigns): register Campaigns app + brand dashboard
Task 8: Per-email report page¶
- Files:
~/forge-suite-wt/campaigns/app/campaigns/email/[id]/page.tsx - What: Detail view for one Mautic email: subject/from/status, sent/read/click/fail counts, Resend delivery/bounce breakdown from
send_events, "Edit in Mautic" deep link (/s/emails/view/<id>). - Verification:
curl -fsS http://127.0.0.1:3061/campaigns/email/4renders the CreatorTrack Updates report. - Commit:
feat(campaigns): per-email report page
Task 9: Systemd timer + trigger script (forge repo)¶
- Files:
/home/justinwieb/forge/scripts/forge_creatortrack_campaigns_jobs.sh,/home/justinwieb/forge/scripts/systemd/forge-creatortrack-campaigns-sync.service,/home/justinwieb/forge/scripts/systemd/forge-creatortrack-campaigns-sync.timer - What: Trigger script curls the prod jobs route (mautic-sync then resend-poll) with the token from
creatortrack-auth.env, mirroringforge_creatortrack_finance_jobs.sh. Timer every 30min. Units NOT enabled until merge to main + prod deploy (note in script header). - Verification:
bash -n scripts/forge_creatortrack_campaigns_jobs.sh+ dry-run against :3061 succeeds. - Commit (forge repo):
feat(campaigns): systemd sync timer + trigger script
Task 10: Register and document¶
- Files:
/home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md,/home/justinwieb/forge/memory/general/reference_creatortrack_campaigns_app.md - What: Topic file (schema, jobs, route, timers, brand-mapping rule, deep-link paths, merge/deploy steps) + MEMORY.md index line. Run
bash /home/justinwieb/forge/scripts/forge_eval_run.sh. - Verification: eval run passes; topic file carries mkdocs URL line.
- Commit (forge repo):
docs(campaigns): register CreatorTrack Campaigns app
Merge / deploy (post-plan, integrator step)¶
Serial AI-reconciled merge of campaigns into main (single merge, per WDA doctrine), apply migration to live lifeos via forge_workspace_migrate.py, restart creatortrack-prod.service, add CAMPAIGNS_JOBS_TOKEN to prod env, enable timer, teardown slot (git -C ~/forge-suite worktree remove ~/forge-suite-wt/campaigns + DROP DATABASE ct_campaigns).