Plan: Creator stack program (JustinWieb + Gus the Bass)¶
URL: https://mkdocs.justinsforge.com/memory/plans/affiliate-click-foundation-2026-07-08/
Date: 2026-07-08
The whole plan (master stack, all layers)¶
This doc is the master plan for the full creator/affiliate stack across both brands. Every layer below is in scope for the program; they ship in phases. Phase 1 (the Link In Bio foundation) is decomposed into runnable tasks; the rest are tracked in the Roadmap and decomposed when reached.
| Layer | Tool | Build or rent | Lives | Justin controls | Michael uses |
|---|---|---|---|---|---|
| Bio page (Link In Bio) | Own "Link In Bio" module | Build | CreatorTrack + CF | schema, renderer, domains | CT drag-drop editor, Gus-scoped |
| Brand websites (gusthebass.com, justinwieb.com) | Own, hand-built | Build (Claude/Justin) | CF Pages | full build + deploy | nothing (NOT Michael-accessible) |
| Storefront | Own storefront module | Build | CreatorTrack + CF | schema, product sync | CT editor, Gus-scoped |
| Link tracking | Owned /r redirect (+ Dub later) |
Build/rent | CF edge (+ Forge/Dub) | worker, KV/D1, Dub keys | nothing (invisible) |
| Affiliate networks | Amazon Assoc (have) + Levanta, ShopMy, Impact | Enroll (upstream) | external accounts | API creds, product sync | picks products from synced catalog |
| Analytics | Own click store + PostHog + Affilimate | Rent, API-first | SaaS → CT view | project keys, event schema | read-only "Gus stats" tab in CT |
| Mautic + Resend (own) | Own | Forge (CT112) | infra, segments, API write | thin "send to Gus list" in CT | |
| CRM / contacts spine | CT CRM / ERPNext / Copper | Already own | CT + Forge | routing, wiring | CT Sales CRM, Gus-scoped |
| Community | Discord + Forge bot | Rent room, own bot | Discord + Forge | the bot, gating | moderates in Discord like a human |
| Course + paywall (later) | Whop | Rent | SaaS → CT | account, Discord-gate, Stripe | Whop creator UI, or CT sales view |
| Payments (later) | Stripe (+ Shopify if physical) | Rent | SaaS | keys, webhooks | nothing / CT sales view |
Access split (load-bearing): The Link In Bio module and the Storefront module are CreatorTrack apps that Michael edits, scoped to the Gus workspace by per-app permission. The brand websites (gusthebass.com, justinwieb.com) are hand-built static sites (Claude + Justin), deployed to Cloudflare Pages, and are not exposed to Michael, they are not CreatorTrack apps. Rule: if Michael changes it, it is a CT app; the full sites are ours to build directly.
Approved spec (Phase 1): A CreatorTrack "Link In Bio" module lets Justin (and Michael, scoped to the Gus workspace) build a mobile bio page (links, affiliate cards, email capture) that publishes to always-on Cloudflare edge, independent of home internet. gusthebass.com/links serves as a static Cloudflare Pages build; link taps go through a /r/<id> Cloudflare Worker (logs the click to D1, then 302s to the affiliate URL, which already carries Justin's Amazon Associates tag); email submits hit a /c Worker (writes to D1). A CreatorTrack cron pulls D1 clicks + captures into the owned contacts spine (CT Postgres) and fans captures out to Mautic per-brand segments, idempotently, so a home outage never loses data or breaks the live page. Public surfaces = Cloudflare (Pages + Workers + D1 + KV). Management + reporting = CreatorTrack on Console. Click tracking is owned, cloud-hosted, and network-agnostic (works for Amazon now, CJ/others later).
Out of scope this round (sketched in Roadmap, decomposed later):
- Amazon PA-API product enrichment + Commission Junction / ShopMy / Impact commission-report sync
- Affilimate integration (commission aggregation)
- The Amazon-style storefront/catalog grid
- justinwieb.com site + its /links
- Dub self-hosted branded short domain (the owned /r redirect already gives tracking; Dub is only for a separate short domain + QR later)
- Course/paywall (Whop/Stripe) and Discord community bot
- Migrating the rest of gusthebass.com off the home static host (only the /links path moves to CF this round)
Justin-only actions (I cannot do these; flagged inline as [JUSTIN]):
- Confirm the Amazon Associates tracking tag(s) to append to product URLs
- Approve the Cloudflare Pages custom-domain DNS change for gusthebass.com/links routing
Architecture (Phase 1)¶
PUBLIC (Cloudflare, always-on)
gusthebass.com/links -> CF Pages (static page, rebuilt on publish; zero runtime dep)
/r/<linkId> -> CF Worker (KV lookup dest -> append click to D1 -> 302) [OWNED tracking]
/c (email capture) -> CF Worker (validate -> append capture to D1 -> 200)
KV links-map: linkId -> {destUrl, brand, label} (read-heavy, rewritten on publish)
D1 events: clicks(append-only), captures(append-only)
HOME (CreatorTrack on Console 192.168.86.50)
app/(suite)/links -> editor (blocks: link / affiliate-card / email-capture), Gus-scoped
publish API -> build static page + link manifest -> deploy Pages + upsert KV
contacts spine (PG) -> links.contacts, links.clicks (mirror), links.captures
sync cron -> pull new D1 rows -> upsert PG spine -> fan captures to Mautic segment
(idempotent by event id; safe to run twice; catches up after outage)
Phase 1 status: DONE + LIVE (2026-07-08)¶
Phase 1 shipped and is live on gusthebass.com. Two deviations from the tasks as written:
- Task 6 (Pages) replaced: the bio page is served by the Worker from KV (page:<slug>), not
a Cloudflare Pages project. The apex is a proxied tunnel-CNAME, so a path-based Pages custom
domain isn't possible; one Worker owning /r, /c, and the page path is simpler with the same
always-on property. No Pages project exists.
- Workspace/brand model refined: both Gus brands live in one Gus The Bass workspace
(lifeos id 85); links.pages.brand distinguishes them. Captures fan out to one combined Mautic
segment 4 "Gus The Bass" tagged brand/source/page. Task 5's Amazon tag is deferred (not
needed to go live; /r tracks any URL). A brand/account picker + gusoutdoor.co second domain
+ "send as either brand" is a follow-on feature (see roadmap), not built.
Everything else (schema, app, editor, publish->KV, D1->CT->Mautic sync cron, verification, registration) is done. Full picture: reference_links_module.
Phase 1 task list¶
Task 1: Provision Cloudflare D1 + KV and record ids¶
- Files: create
/home/justinwieb/forge/infra/links-edge/README.md(records D1 db id, KV namespace id, account id, worker name, routes);URL: https://mkdocs.justinsforge.com/infra/links-edge/README/line at top is NOT needed (infra/ is an excluded/bare-path dir). - What: Use
wrangler(auth via~/.forge-secrets/cloudflare.env) to create D1 databaselinks-eventsand KV namespacelinks-map. Capture their ids into the README. - Verification:
cd /home/justinwieb/forge/infra/links-edge && npx wrangler d1 list | grep links-events && npx wrangler kv namespace list | grep links-map - Commit:
feat(links-edge): provision cloudflare d1 + kv for click tracking
Task 2: Scaffold the edge Worker project¶
- Files:
/home/justinwieb/forge/infra/links-edge/wrangler.toml,/home/justinwieb/forge/infra/links-edge/package.json,/home/justinwieb/forge/infra/links-edge/src/index.ts(stubfetchreturning 200 on/health),/home/justinwieb/forge/infra/links-edge/.gitignore. - What: Minimal Workers project bound to the D1 db + KV namespace from Task 1 (bindings in
wrangler.toml). Router stub only. - Verification:
cd /home/justinwieb/forge/infra/links-edge && npx wrangler deploy --dry-run 2>&1 | grep -i "Total Upload\|success" - Commit:
feat(links-edge): scaffold worker project with d1 + kv bindings
Task 3: Implement the /r/<id> click-redirect Worker¶
- Files:
/home/justinwieb/forge/infra/links-edge/src/index.ts(edit),/home/justinwieb/forge/infra/links-edge/src/redirect.ts(new). - What:
GET /r/:id-> readlinkIdfrom KVlinks-map; if missing, 404. Else append a row to D1clicks(id=ULID, link_id, brand, ts, ua, referer, country fromcf) viactx.waitUntil(never block the redirect), then302todestUrl. Fail-open: if D1 write throws, still redirect (log-and-continue,# Why swallowed:the redirect must never break on a logging error). - Verification:
cd /home/justinwieb/forge/infra/links-edge && npx wrangler dev --local &thencurl -sI localhost:8787/r/testseed | grep -i location(seed a KV+D1 test row in the test setup); assert 302 + Location. - Commit:
feat(links-edge): /r click redirect with fail-open d1 logging
Task 4: Implement the /c email-capture Worker¶
- Files:
/home/justinwieb/forge/infra/links-edge/src/index.ts(edit),/home/justinwieb/forge/infra/links-edge/src/capture.ts(new). - What:
POST /cJSON{email, brand, source, pageId}-> validate email shape (reject loud on bad input, 400) -> append to D1captures(id=ULID, email, brand, source, ts, consumed=false). CORS locked to the brand domains. Rate-limit per-IP via KV counter (fail loud past threshold, 429). - Verification:
curl -s -XPOST localhost:8787/c -H 'content-type: application/json' -d '{"email":"[email protected]","brand":"gus","source":"links","pageId":"p1"}' | jq .okreturnstrue; a second malformed body returns 400. - Commit:
feat(links-edge): /c email capture worker with validation + rate limit
Task 5: Deploy the Worker + wire production routes¶
- Files:
/home/justinwieb/forge/infra/links-edge/wrangler.toml(edit: add routesgusthebass.com/r/*,gusthebass.com/c),/home/justinwieb/forge/infra/links-edge/README.md(edit: record live routes). - What:
wrangler deploy. Add the two routes on thegusthebass.comzone. [JUSTIN] confirm no conflict with the existing static site routing (only/r/*and/cgo to the Worker; everything else stays on the current static host). - Verification:
curl -sI https://gusthebass.com/r/testseed | grep -i locationreturns the seeded destination over the live edge. - Commit:
feat(links-edge): deploy worker + gusthebass /r and /c routes
Task 6: Cloudflare Pages project for the static page¶
- Files:
/home/justinwieb/forge/infra/links-edge/pages/README.md(records Pages project name + custom domain mapping). - What: Create a CF Pages project
gus-links. Map custom domain sogusthebass.com/linksserves the Pages build (path-based; the rest of the apex stays on the current static host). [JUSTIN] approve the DNS/routing change. Seed with a placeholderindex.htmlto prove the route. - Verification:
curl -s https://gusthebass.com/links | grep -i "gus"returns the placeholder page from the edge. - Commit:
feat(links-edge): cloudflare pages project + gusthebass.com/links route
Task 7: CreatorTrack DB migration, Links schema + contacts spine¶
- Files:
/home/justinwieb/forge-suite/db/schema.ts(edit: addlinkstables), new migration/home/justinwieb/forge-suite/db/migrations/<ts>_feat_links_module_pages_blocks_contacts_clicks_captures.sql. - What: Tables:
links.pages(id, workspace_id, brand, slug, title, theme, published_at),links.blocks(id, page_id, kind, position, data jsonb),links.contacts(id, workspace_id, email, first_source, tags text[], created_at, unique(workspace_id,email)),links.clicks(id pk, link_id, brand, ts, ua, country)(mirror of D1),links.captures(id pk, email, brand, source, ts, synced_to_mautic bool).idcols use the D1 ULIDs so sync is idempotent upsert. - Verification: apply migration on dev DB, then
psql "$CT_DEV_DATABASE_URL" -c "\dt links.*"lists all five tables. - Commit:
feat(links): db schema for pages, blocks, contacts, clicks, captures
Task 8: Register the Link In Bio app + scaffold the route¶
- Files:
/home/justinwieb/forge-suite/lib/apps.ts(edit: add{id:"link-in-bio", name:"Link In Bio", route:"/link-in-bio", ...}entry),/home/justinwieb/forge-suite/app/(suite)/link-in-bio/page.tsx(new: shell that lists the workspace's pages). - What: Add Link In Bio to the app catalog so it shows in the App Library + sidebar. Scaffold the app shell reading
links.pagesfor the current workspace (DB namespace stayslinks.*internally; user-facing name is Link In Bio). Respect per-app permission so Michael sees it only for Gus. - Verification:
cd /home/justinwieb/forge-suite && npx tsc --noEmit 2>&1 | tail -3clean; dev server renders/link-in-biolisting (covered by Task 12 e2e). - Commit:
feat(link-in-bio): register app + route shell
Task 9: Link In Bio editor UI (blocks: link, affiliate-card, email-capture)¶
- Files:
/home/justinwieb/forge-suite/app/(suite)/link-in-bio/[pageId]/page.tsx(new editor),/home/justinwieb/forge-suite/app/(suite)/link-in-bio/components/BlockEditor.tsx(new),/home/justinwieb/forge-suite/app/api/links/route.ts(new: CRUD pages/blocks). - What: Drag-order list of blocks. Block kinds:
link(label+url),affiliate-card(label, destUrl already carrying [JUSTIN]'s Amazon tag, image, price),email-capture(headline, button text, brand target). Autosave tolinks.blocksvia the API. Use thefrontend-designskill for the editor look; match existing CT app styling. - Verification:
curl -s -XPOST http://127.0.0.1:3066/api/links -H 'content-type: application/json' -d '{"action":"addBlock","pageId":"<id>","kind":"link","data":{"label":"YT","url":"https://youtube.com"}}' | jq .okreturns true and the row appears inlinks.blocks. - Commit:
feat(links): block editor + pages/blocks CRUD api
Task 10: Publish pipeline (build static page + push to Pages + upsert KV)¶
- Files:
/home/justinwieb/forge-suite/app/api/links/publish/route.ts(new),/home/justinwieb/forge-suite/lib/links/render.ts(new: page-config -> static HTML),/home/justinwieb/forge-suite/lib/links/cf.ts(new: Pages deploy + KV upsert via CF API). - What: Publish action for a page: (1) render blocks to a self-contained static HTML (affiliate links rendered as
/r/<linkId>, capture form POSTs to/c), (2) deploy the HTML to thegus-linksPages project, (3) upsert eachlinkId -> {destUrl,brand,label}into KVlinks-map, (4) stamplinks.pages.published_at. Idempotent: same page republished = same linkIds, KV overwrite. - Verification: trigger publish for the Gus page, then
curl -sI https://gusthebass.com/r/<realLinkId> | grep -i locationshows the affiliate destination, andcurl -s https://gusthebass.com/links | grep "/r/"shows the rendered tracked links. - Commit:
feat(links): publish pipeline to cloudflare pages + kv link map
Task 11: D1 -> CT sync cron + Mautic fan-out¶
- Files:
/home/justinwieb/forge/scripts/forge_links_sync.py(new),/home/justinwieb/forge/infra/systemd/forge-links-sync.service+.timer(new, every 10 min). - What: Pull new D1
clicks+captures(by max-seen id watermark) via the CF D1 HTTP API; upsert intolinks.clicks/links.captures(idempotent on ULID pk); for each unsynced capture, create/update the Mautic contact in the brand segment via Mautic API, then setsynced_to_mautic=true. Fail loud on API errors (retry next tick; no silent drop). Usesdatefrom an injected clock, notdate.today()(perfeedback_date_today_banned_in_forge_scripts). - Verification:
python3 /home/justinwieb/forge/scripts/forge_links_sync.py --oncethenpsql "$CT_DEV_DATABASE_URL" -c "select count(*) from links.captures where synced_to_mautic"is >0 after a test capture, and the contact exists in Mautic. - Commit:
feat(links): d1->ct sync cron with mautic fan-out
Task 12: End-to-end verification on the live Gus page¶
- Files:
/home/justinwieb/forge/scripts/forge_links_smoke.sh(new: drives the full loop). - What: Script: publish the Gus page ->
curla real/r/<id>(assert 302 + click row lands in D1 then CT after sync) ->POST /ca test email (assert capture row -> CT contact -> Mautic segment). Confirms the outage-resilient path: kill the sync timer, verify/r+/cstill work on the edge, restart timer, verify catch-up. - Verification:
bash /home/justinwieb/forge/scripts/forge_links_smoke.shexits 0 with all assertions green. - Commit:
test(links): end-to-end smoke for publish, click, capture, sync
Task 13: Register and document¶
- Files:
/home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md(index entry),/home/justinwieb/forge/memory/general/reference_links_module.md(new topic file withURL:line), update/home/justinwieb/forge/memory/plans/affiliate-click-foundation-2026-07-08.md(mark Phase 1 done). - What: Document the architecture, the D1/KV/Pages ids, the sync cron, and the Justin-vs-Michael control model. Add MEMORY.md index line. Run the eval harness.
- Verification:
bash /home/justinwieb/forge/scripts/forge_eval_run.shpasses;grep -q links_module /home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md. - Commit:
docs(links): register Links module + reference topic + memory index
Roadmap (every stack layer, decomposed when we reach it)¶
Maps 1:1 to the master stack table. Each becomes its own /feature-plan when we start it.
Phase 2: Storefront module (stack row: Storefront)¶
- Amazon-style catalog grid at
gusthebass.com/shop(same edge pattern, second Pages route). - New CT app or block kind
storefront-section;links.productstable. Michael-editable, Gus-scoped. - Amazon PA-API product enrichment (image/title/price) [JUSTIN: Amazon Associates tag + PA-API keys].
- Reuse
/rtracking; products are affiliate-cards in a grid.
Phase 3: Brand websites, hand-built (stack row: Brand websites) — NOT Michael-accessible¶
gusthebass.comrebuilt as a proper site (currently thin static on the home host) andjustinwieb.combuilt from scratch (currently nonexistent, on Beacons).- Built by Claude + Justin directly, deployed to Cloudflare Pages (always-on, off the home host). These are NOT CreatorTrack apps and Michael has no editor for them.
- The
/linksand/shoppaths (Michael's modules) mount into these sites via the CF routing already established in Phase 1. - Use the
frontend-designskill; each brand gets its own look.
Phase 4: Analytics + commission-report sync (stack rows: Analytics)¶
- Own click store already live (Phase 1). Add PostHog cloud event instrumentation on the pages for funnels (land -> capture -> click-out).
- Pull conversions/commissions from Amazon Associates report + Commission Junction API into
links.commissions; optional Affilimate to aggregate all networks in one pull. - Read-only "Gus stats" tab in CT: click -> capture -> commission funnel per link (the thing Beacons can't do). Michael sees the 5 numbers that matter; never logs into PostHog.
Phase 5: JustinWieb second-brand clone¶
- Point the Phase 1-4 machinery at the
justinwiebworkspace/brand; second Pages project +/r,/croutes onjustinwieb.com. - Michael stays Gus-scoped; JustinWieb is Justin-only.
Phase 6: Community (stack row: Community) — separate plan¶
- Discord server per brand + a Forge bot: bio-page "Join the Discord" funnels members in; bot syncs members to the CT contacts spine, gates channels by role.
- Michael moderates in Discord like a human; the bot is Justin's.
Phase 7: Course + paywall + payments (stack rows: Course, Payments) — separate plan, only when there is a paid product¶
- Free lead-magnet course = email-gated videos on the brand site (no platform). Paid = Whop (checkout + Discord-gate + course host) or Stripe + own course page.
- Stripe for money; Shopify only if a brand sells physical goods with inventory. Sales land in ERPNext/CT for the ledger.
Phase 8 (optional polish): Dub branded short domain + QR¶
- Stand up Dub (self-hosted on Forge or cloud) for a branded short domain (
gus.link) + QR codes on packaging/in-video. The owned/rredirect already tracks; Dub is only for the separate short domain + QR surface.
Notes on the control model (why this shape)¶
- Justin = root/infra: owns the Worker, D1/KV, Pages project, the sync cron, CF API, Amazon tag. Works from Forge CLI +
wrangler. - Michael = CreatorTrack UI only: the Links editor app, Gus-scoped by per-app permission. He never touches Cloudflare, D1, or a terminal; he edits blocks and hits Publish. A home outage stops his editing, not the live page or tracking.
- Robustness is the reason public surfaces live on Cloudflare edge and only management lives on home Console; matches the approved spec.