Skip to content

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
Email 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 database links-events and KV namespace links-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 (stub fetch returning 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 -> read linkId from KV links-map; if missing, 404. Else append a row to D1 clicks (id=ULID, link_id, brand, ts, ua, referer, country from cf) via ctx.waitUntil (never block the redirect), then 302 to destUrl. 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 & then curl -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 /c JSON {email, brand, source, pageId} -> validate email shape (reject loud on bad input, 400) -> append to D1 captures (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 .ok returns true; 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 routes gusthebass.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 the gusthebass.com zone. [JUSTIN] confirm no conflict with the existing static site routing (only /r/* and /c go to the Worker; everything else stays on the current static host).
  • Verification: curl -sI https://gusthebass.com/r/testseed | grep -i location returns 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 so gusthebass.com/links serves 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 placeholder index.html to 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
  • Files: /home/justinwieb/forge-suite/db/schema.ts (edit: add links tables), 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). id cols 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
  • 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.pages for the current workspace (DB namespace stays links.* 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 -3 clean; dev server renders /link-in-bio listing (covered by Task 12 e2e).
  • Commit: feat(link-in-bio): register app + route shell
  • 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 to links.blocks via the API. Use the frontend-design skill 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 .ok returns true and the row appears in links.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 the gus-links Pages project, (3) upsert each linkId -> {destUrl,brand,label} into KV links-map, (4) stamp links.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 location shows the affiliate destination, and curl -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 into links.clicks / links.captures (idempotent on ULID pk); for each unsynced capture, create/update the Mautic contact in the brand segment via Mautic API, then set synced_to_mautic=true. Fail loud on API errors (retry next tick; no silent drop). Uses date from an injected clock, not date.today() (per feedback_date_today_banned_in_forge_scripts).
  • Verification: python3 /home/justinwieb/forge/scripts/forge_links_sync.py --once then psql "$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 -> curl a real /r/<id> (assert 302 + click row lands in D1 then CT after sync) -> POST /c a test email (assert capture row -> CT contact -> Mautic segment). Confirms the outage-resilient path: kill the sync timer, verify /r + /c still work on the edge, restart timer, verify catch-up.
  • Verification: bash /home/justinwieb/forge/scripts/forge_links_smoke.sh exits 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 with URL: 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.sh passes; 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.products table. Michael-editable, Gus-scoped.
  • Amazon PA-API product enrichment (image/title/price) [JUSTIN: Amazon Associates tag + PA-API keys].
  • Reuse /r tracking; products are affiliate-cards in a grid.

Phase 3: Brand websites, hand-built (stack row: Brand websites) — NOT Michael-accessible

  • gusthebass.com rebuilt as a proper site (currently thin static on the home host) and justinwieb.com built 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 /links and /shop paths (Michael's modules) mount into these sites via the CF routing already established in Phase 1.
  • Use the frontend-design skill; 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 justinwieb workspace/brand; second Pages project + /r,/c routes on justinwieb.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 /r redirect 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.