URL: https://mkdocs.justinsforge.com/memory/handoffs/links-storefront-status-2026-07-11/
Links storefront worker: status, 2026-07-11¶
Built Phase 2 (Storefront module) of the
Link In Bio module per
affiliate-click-foundation.
The storefront is a new block kind inside the existing Link In Bio app, not a separate
CreatorTrack app: links.pages already supports an arbitrary slug/host/publish path
(page:<host>:<slug> KV key, per-page ensureRoute), so a "storefront page" is just a
Links page with slug shop and a storefront-section block on it. This kept the change
additive to render.ts/cf.ts rather than requiring a new publish mechanism.
Read this whole file before going live — it documents an incident (see below) where the first end-to-end test accidentally published to real production Cloudflare, and the cleanup performed.
Branches (NOT merged, NOT deployed, NOT prod-migrated)¶
Two separate repos, both branch storefront:
- CreatorTrack repo, worktree
/home/justinwieb/forge-suite-wt/storefront, branchstorefront. All app/schema/API code. forgerepo, branchstorefront(this repo):infra/links-edge/wrangler.tomlonly (the/shopstatic route declarations).forge'smainhad pre-existing unrelated local modifications (LESSONS.md, memory/, brands/, etc — not mine, likely another session's WIP); I stashed them, branched off main, applied only mywrangler.tomldiff, committed, then restored main's original dirty state (git stash pop) so nothing of the pre-existing WIP was lost or committed by me.
Files changed¶
CreatorTrack worktree (branch storefront)
- db/migrations/20260711T221711_storefront_links_products_table_storefront_section_block_kind.sql
(new) — links.products table (RLS) + widens blocks_kind_check to admit
storefront-section.
- db/schema.ts (edit, additive) — linkProducts drizzle table + LinkProduct type.
- lib/links-store.ts (edit) — storefront-section added to BlockKind / BLOCK_KINDS.
- lib/links-products-store.ts (new) — RLS-scoped CRUD over links.products
(listProducts/createProduct/updateProduct/deleteProduct/reorderProducts) + a local
Crockford-base32 ULID generator (no new npm dependency).
- components/apps/link-in-bio/storefrontGrid.ts (new) — pure shared module (client + server
safe), mirrors publishedGrid.ts's snapshot pattern: StorefrontItem type, storefrontItems/
storefrontVisible/snapshotFromProduct/mergeStorefrontItems/formatPrice, plus
STOREFRONT_CSS (the Amazon-style grid styles).
- components/apps/link-in-bio/StorefrontFields.tsx (new) — editor pane: heading input,
"Refresh from products" (pulls the workspace's catalog and merges, preserving order/hidden
flags), an inline "Add product" mini-form (title/image/price/destUrl/ASIN) that creates the
product via the API and appends it to the snapshot, and an "Autofill from ASIN" button wired
to the PA-API scaffold.
- components/apps/link-in-bio/BlockEditor.tsx (edit, additive) — storefront-section in
BLOCK_META/ADD_ORDER/BlockFields/defaultData; threads the page's brand down to
BlockFields (new prop, needed for product creation).
- components/apps/link-in-bio/LinkBioPreview.tsx (edit, additive) — storefront-section
render case + ProductCard (image/title/price, /r/<linkId> in publish mode).
- components/apps/link-in-bio/link-in-bio.css (edit, additive) — .lib-storefront-add +
one selector tweak; reuses the existing .lib-pgrid-item* classes for the item list.
- lib/links/render.ts (edit, additive) — deriveProductLinkId(brand, blockId, productId)
(mirrors deriveGridItemLinkId), a buildLinkMap case for storefront-section that stamps
a linkId per visible product, STOREFRONT_CSS inlined into the published page's <head>.
- lib/secrets.ts (edit, additive) — amazonPaapiEnv() reading the optional
~/.forge-secrets/amazon-paapi.env file (same loadEnvFileOptional pattern as cloudflareEnv/
giphyApiKey).
- lib/links/paapi.ts (new) — PA-API 5.0 GetItems scaffold: hand-rolled AWS SigV4 signer (no
aws-sdk dependency for one endpoint), isAmazonPaapiConfigured(), enrichFromAsin(asin).
UNTESTED against real Amazon credentials (none exist in this environment) — the signer
follows the published PA-API 5.0 spec exactly but has not been run against a live ASIN. Verify
the first time real keys are dropped in.
- app/api/links/products/route.ts (new) — links.products data plane: GET list (scoped to
the caller's active workspace regardless of the workspaceId query param), POST action
dispatch (create/update/delete/reorder). Same allowWorkspaceSlugs gate as
app/api/links/route.ts (reuses the link-in-bio app definition — storefront is not a
separate app).
- app/api/links/products/enrich/route.ts (new) — POST { asin } → PA-API lookup. Returns
{ ok:false, reason:"not_configured" } (200, not an error) when the secrets file is absent;
any other PA-API failure is a 502 with the real error message (fail loud, never a silent
"not found").
forge repo (branch storefront)
- infra/links-edge/wrangler.toml (edit) — static [[routes]] for gusthebass.com/shop and
gusoutdoor.co/shop. No Worker source change needed: src/page.ts already serves any
page:<host>:<slug> KV key for any slug, so /shop reaches the exact same code path /links
does. This is a documentation/robustness addition (so /shop doesn't depend on the publish
pipeline's runtime ensureRoute() call succeeding), not a functional requirement.
Schema: links.products¶
Workspace-scoped + RLS, matching the existing links.* pattern.
| Column | Type | Notes |
|---|---|---|
id |
text PK | ULID, minted app-side (lib/links-products-store.ts) |
workspace_id |
bigint | FK core.workspaces(id) |
brand |
text | matches links.pages/clicks/captures.brand convention |
title |
text | |
image_url |
text | |
price_cents |
bigint | |
currency |
text | default USD |
dest_url |
text | affiliate URL, already carrying the tracking tag |
external_asin |
text, nullable | only used by the PA-API enrichment scaffold |
position |
integer | catalog grid order |
created_at / updated_at |
timestamptz |
links.blocks.blocks_kind_check widened to admit storefront-section alongside the existing
seven kinds.
How it fits together (data flow)¶
- Michael (or Justin) creates a Links page, sets its slug to
shopand brand to a Gus brand. - Adds a "Storefront" block. In the editor: "Add product" (writes to
links.products+ snapshots into the block) or "Refresh from products" (pulls the whole catalog and merges). - Hit Publish.
renderPage()(unchanged call site,lib/links/render.ts) walks the block, stamps alinkIdper visible product (gusthebass-<blockId>-p<productId>, deterministic ⇒ idempotent republish), and renders an Amazon-style grid (STOREFRONT_CSS). lib/links/cf.ts(unchanged) writes the link map +page:gusthebass.com:shopHTML to KV and callsensureRoutefor theshopslug — exactly the same path/linksalready uses.- A tap on a product card hits
/r/<linkId>on the edge Worker (unchanged), which logs the click to D1 and 302s to the product'sdest_url— the same tracking/linksaffiliate-cards get, zero new plumbing.
Verification performed (real, against the dev clone) — and an incident¶
npx tsc --noEmit: clean except 5 pre-existing, unrelated errors (missingpublic/brand/*.pngassets, a globalLayoutPropstype) present in files I never touched — confirmed pre-existing bygit status(none of those files appear in my changed-file list).- Migration applied cleanly to the
ct_storefrontdev clone (\d links.productsconfirmed all columns, the FK, the index, and theworkspace_isolationRLS policy). - Started
next dev --webpack -p 3067against the worktree withWORKSPACE_DEV_AUTH=1. Created a page (brand:"gusthebass",slug:"shop"), created two test products viaPOST /api/links/products, added astorefront-sectionblock with both products, then hitGET /api/links/publish?pageId=3(dry-render): confirmed the rendered HTML containslbp-shop-grid/lbp-shop-card, both product titles/prices, and both hrefs as/r/gusthebass-<blockId>-p<productId>(tracked, deterministic). - Incident: the shell I launched the dev server from had earlier
sourced~/.forge-secrets/creatortrack-auth.env, which — unlike the isolated dev-testing recipe inreference_creatortrack_dev_testing.md— also exportsLINKS_PUBLISH_LIVE=1and a real Cloudflare API token (apparently meant for the prod systemd service's environment, not for ad hoc dev shells). Those inherited into thenext devprocess. My subsequentPOST /api/links/publish {pageId:3}therefore went live: it wrotepage:gusthebass.com:shop(fake test HTML) to the reallinks-mapKV namespace, wrote KVlinkId → destUrlentries for the two fake test products (amazon.com/dp/B000TEST1/2), and added a realgusthebass.com/shoproute on the live Cloudflare zone pointing at thelinks-edgeWorker. - Caught and reverted immediately: killed the dev server, then used the real
~/.forge-secrets/cloudflare.envcredentials to (a) delete thegusthebass.com/shopWorker route via the CF API, and (b) delete the three KV keys I had written (page:gusthebass.com:shopand the two testlinkIdentries). Verified viaGET .../workers/routesthat the route list matches its pre-incident state (/linksy,/link,/links,/c,/r/*only — the first two are pre-existing from an unrelated earlier session, not mine, left untouched). No real Gus data was ever live; only my two fake test products (fake ASINs, fake Amazon URLs) were exposed, for well under a minute. - Root cause: I inherited
LINKS_PUBLISH_LIVE=1+ real CF creds fromcreatortrack-auth.envinto a dev shell instead of using an isolated env (the documented dev-testing recipe only sources that file forAUTH_SECRET/OAuth vars and does not warn that it also carries live-publish flags). Restarted the dev server in a clean shell with those vars explicitlyunsetand confirmedenv | grep -E "LINKS_PUBLISH_LIVE|CLOUDFLARE"printed nothing before touching publish again. - Re-ran the same publish call in the clean-env dev server:
POST /api/links/publishnow correctly returned{ok:true, live:false, dryRun:true, linkCount:2, note:"LINKS_PUBLISH_LIVE not set..."}— confirmed safe-by-default behavior restored. POST /api/links/products/enrich {asin:"B000TEST1"}→{ok:false, reason:"not_configured"}(no~/.forge-secrets/amazon-paapi.envpresent) — confirmed the documented no-op path.- Test products remain in the isolated
ct_storefrontdev DB clone only (never touched prod Postgres); harmless, will be swept when the dev-slot reaper eventually reaps this worktree.
Recommendation: consider splitting creatortrack-auth.env so LINKS_PUBLISH_LIVE +
Cloudflare creds live in a separate prod-only env file, not one that gets casually sourced for
dev-auth testing. Flagging this as a process gap, not fixing it myself (out of scope / touches
shared secrets layout Justin owns).
Blocked-on-Justin¶
- Amazon PA-API keys (for image/title/price autofill): drop
~/.forge-secrets/amazon-paapi.envwithAMAZON_PAAPI_ACCESS_KEY,AMAZON_PAAPI_SECRET_KEY,AMAZON_PAAPI_PARTNER_TAG(the Associates tag), optionallyAMAZON_PAAPI_HOST/AMAZON_PAAPI_REGION. Until then, "Autofill from ASIN" shows a friendly "not configured" message and manual entry works fully. - Confirm
gusthebass.com/shop(andgusoutdoor.co/shop) 404 on the current tunnel-served static site before deploying the Worker route (same check already done for/linksper the existingwrangler.tomlcomment) — I did not verify this myself (would require touching the live site), it's called out inline in the newwrangler.tomlcomment. wrangler deployforlinks-edge(attaches the new/shoproutes) — this worker never runs that; per the incident above, the route already existed briefly and was cleaned up, so deploying now is purely to make the (already-proven-working)/shoppath durable again.
Go-live steps (Justin, when ready)¶
- Review + merge the CreatorTrack repo PR (
storefrontbranch) to main. - Review + merge the
forgerepo PR (storefrontbranch,wrangler.tomlonly) to main. - Apply the migration to prod by hand (this worker never runs prod migrations):
- Deploy CreatorTrack per the normal path (
scripts/forge_creatortrack_deploy.sh). - Verify item 2 above (both
/shoppaths 404 on the current static sites), thencd forge/infra/links-edge && set -a; source ~/.forge-secrets/cloudflare.env; set +a && npx wrangler deployto attach the/shoproutes. - Build the real Gus storefront page in CreatorTrack (
/link-in-bio, Gus The Bass workspace): create/rename a page to slugshop, add real products (manually or once PA-API keys land), add the Storefront block, hit Publish (now live with real creds + the flag set only in prod). - When Amazon PA-API keys are available, drop them into
~/.forge-secrets/amazon-paapi.envand do a first realenrichFromAsin()call against a known ASIN to confirm the SigV4 signer works end-to-end (flagged UNTESTED above).