Frontend Apps
Every frontend surface — the claim worker, client portal, marketing site, docs, this wiki, the mock API — plus the server-rendered admin UI.
Frontends live under frontend/. Deploy targets split three ways: Cloudflare Workers (grasshopper, mocky-balboa), Cloudflare Pages static export (octopus-website, octopus-docs), and systemd Node standalone on the VMs (client portal). The admin UI is not a JS app at all — it's server-rendered Jet templates inside the Go binary.
Everything here uses bun (bun install, bun run dev, bunx). Several READMEs are generator boilerplate that say npm — ignore them.
Summary
| App | Domain | Framework | Deploy target | Local dev |
|---|---|---|---|---|
| grasshopper | claim.octopuscards.io (+ sandbox-claim.*) | Hono 4 + JSX, Drizzle + D1 | Cloudflare Workers (wrangler deploy) via GHA | bun run dev (:8787) |
| client | app.octopuscards.io (+ sandbox-app.*) | Next.js 15.5 (standalone), React 19, MUI 7 | systemd Node on the VMs | bun run dev (:3001) |
| octopus-website | octopuscards.io, www.* | Next.js 16 + Fumadocs, static export | Cloudflare Pages via GHA | bun run dev (:3000) |
| octopus-docs | developer.octopuscards.io | Next.js 16 + Fumadocs + Mermaid, static export | Cloudflare Pages via GHA | bun run dev (:3002) |
| handover-wiki | (none yet — this wiki) | Next.js 16 + Fumadocs (base-ui) | No pipeline yet (next start) | bun run dev (:3000) |
| mocky-balboa | mocky-balboa.octopuscards.io | Hono 4, Drizzle + D1 | Cloudflare Workers via GHA | bun run dev (:8788) |
All CI workflows are path-filtered under frontend/<app>/** on master, use bun, and share CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID secrets.
grasshopper — the claim worker
Public voucher/eSIM claim & redemption worker. Routes claim.octopuscards.io/* (prod) and sandbox-claim.octopuscards.io/*. Entry src/index.tsx. Handles voucher claims, contact form, status polling, and a bearer-protected admin panel.
- Stack: Hono 4 + JSX on Cloudflare Workers (
nodejs_compat), Drizzle ORM over Cloudflare D1 (SQLite), pino,qrcode-generator, OTel via@microlabs/otel-cf-workers→https://otel.octopuscards.io/v1/traces. - Talks to the Go backend via
OCTOPUS_API_URLusingOCTOPUS_CLIENT_USERNAME/PASSWORD, caching the Octopus JWT in theoctopus_tokenKV to reduce/auth/loginfan-out from cold isolates. - D1: binding
DB, databasegrasshopper(d99ec4a1-5833-48ca-8eb6-49568a13d5fd); sandboxgrasshopper_sandbox(d0f8b860-…). Migrations indrizzle/migrations/(0000→0020). - Rate limiters (Workers bindings, not KV):
CLAIM(5/60s per code),CLAIM_IP(20/60s),CONTACT(3/60s),STATUS(5/60s),ADMIN(10/60s). - Secrets (via
wrangler secret put):OCTOPUS_API_URL,OCTOPUS_CLIENT_USERNAME/PASSWORD,ENCRYPTION_KEY(AES-256-GCM PII field encryption — the worker crashes without it),ADMIN_TOKEN,CLAIM_NONCE_SECRET(HMAC),TURNSTILE_SECRET_KEY.
cd frontend/grasshopper && bun install
bun run dev # wrangler dev :8787 (.dev.vars for secrets)
bun run db:generate # drizzle-kit generate
bun run migrate:local # apply D1 migrations locally
bun run deploy # wrangler deploy --minifyDeploy: GHA deploy-grasshopper.yml on push under frontend/grasshopper/** → bun install → apply remote D1 migrations → wrangler deploy. workers_dev: false (custom routes only). Sandbox is a separate workflow (--env sandbox, isolated D1/KV/rate-limit namespaces).
The env.sandbox.vars block in grasshopper/wrangler.jsonc has several sensitive-looking values (sandbox encryption key, admin token, Turnstile secret, nonce secret, demo Octopus creds) committed in git. Prod keeps these as real Wrangler secrets. Rotate the sandbox ones if that repo ever goes public.
client — the client portal
Authenticated client/partner panel (Aurora template). SSR against the Go backend with httpOnly-cookie auth. Products, orders, data-grids, charts, WebAuthn passkeys, i18n, maps.
- Stack: Next.js 15.5 App Router, React 19, MUI 7 + Emotion, SWR + axios, react-hook-form + yup, echarts, mapbox-gl, OTel web SDK.
output: 'standalone'— do not switch tooutput: 'export'; dynamic routes like/products/[id]need SSR. - Deploy = systemd Node standalone (NOT Cloudflare). Built with bun, run with Node.
deploy/update-octopus-client.shbuilds and copies.next/standalone+.next/static+publicinto/opt/octopus-client, served byoctopus-client.service→/usr/bin/node /opt/octopus-client/server.json:3001(sandbox:3002).
Why Node, not Bun, at runtime
Bun's Node-compat getPrototypeOf shim crashes Next.js 15.5.x SSR on every dynamic route. Bun is fine for bun install / bun run build; the runtime must be Node (NodeSource; deploy scripts symlink /usr/bin/node if missing). This is hardcoded in the systemd unit.
All NEXT_PUBLIC_* values (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_BRAND, mapbox token, OTel endpoint) are inlined at build time — changing any requires a full rebuild via update-octopus-client.sh. NEXT_PUBLIC_BRAND (default | octopus-cards) drives white-labelling. MUI Stack defaults to direction="row" in this app's theme — always set direction="column" explicitly for vertical layouts (house rule).
octopus-website — marketing site
Public landing site for octopuscards.io. Next.js 16 + Fumadocs, Tailwind 4, output: 'export' (static out/), Takumi for build-time OG images, Orama search. Build injects NEXT_PUBLIC_CLAIM_HOST=https://claim.octopuscards.io and a Turnstile key. Deploy: GHA deploy-octopus-website.yml → wrangler pages deploy out --project-name=octopus-website. Deployed at apex; see the Memory Appendix.
octopus-docs — developer docs
Developer/API docs at developer.octopuscards.io. Next.js 16 + Fumadocs + Mermaid, static export. next.config.mjs inlines white-label/feature-flag build env (FEATURE_VOUCHERS/TOPUPS/ESIM_ENABLED, APP_NAME, APP_API_HOST, APP_SANDBOX_API_HOST, APP_SITE_URL, icons) that mirror the backend flags so partner deploys stay consistent. Deploy: GHA deploy-octopus-docs.yml → wrangler pages deploy out --project-name=octopus-docs. Nav is driven by top-level content/docs/meta.json; note the dev footer-cache gotcha after meta edits (Memory Appendix).
There's a second, alternative delivery path for the docs: deploy/setup-octopus-docs.sh / update-octopus-docs.sh build the static out/ and serve it from /opt/octopus-docs behind nginx (__DOCS_DOMAIN__). The live developer docs use Cloudflare Pages (developer.octopuscards.io → octopus-docs.pages.dev); the nginx path exists for self-hosted/partner deploys.
handover-wiki — this wiki
Internal handover knowledge base (the app you're reading). Next.js 16 + Fumadocs using @fumadocs/base-ui. No output: export — a normal SSR app. No CI workflow and no wrangler config yet — deploy target is unwired. Run locally with bun run dev. If you want it hosted, the simplest path is a new Cloudflare Pages project (it's Fumadocs like the others, but currently SSR — switch to static export or use @opennextjs/cloudflare).
mocky-balboa — mock Octopus/vendor API
Mock server standing in for external APIs, used by CI contract tests (-tags=contract against https://mocky-balboa.octopuscards.io) and local/integration runs. Hono 4 on Workers, Drizzle + D1 (mocky, 6dad79fb-db2d-43df-b6eb-96d27b34b89e). Deploy: GHA deploy-mocky-balboa.yml → apply remote D1 migrations → wrangler deploy --minify. The mockychaos helper in test/testhelpers/ drives its chaos hooks (4xx/5xx/timeout/malformed/drop).
Admin UI — server-rendered Jet (not a JS app)
The admin panel is inside the Go binary, rendered from Jet templates in views/:
views/admin/*.jet— ~50 pages (orders, products, vendors, inventory dashboard, job executions, login + 2FA setup, grasshopper products, eSIM order detail, …).views/layouts/,views/partials/,views/email/.
Served from warden.octopuscards.io (→ the Go backend, /admin + /assets only). No separate build or deploy — it ships with the binary.
Admin UI house rules
Never use alert() — use showToast(message, type) from views/layouts/admin/base.jet. Never use confirm() — use await confirmAction({title, body, confirmLabel, confirmVariant, icon}). See Memory Appendix.
G2A Import Channel
The inbound G2A marketplace channel — Octopus is the server. OAuth2 token issuance, the reserve → order → pull-codes lifecycle, mint-on-demand GH stock, per-tenant scoping, and its strong test coverage.
Client Portal — Architecture
The client-facing Next.js portal — App Router structure, the eleven-deep provider stack, the hand-written axios data layer (and the orphaned SWR template leftovers), MUI 7 + Aurora theming with the Stack-row-default gotcha, i18n/OTel, and the build/deploy reality (standalone Node under systemd + nginx — NOT Cloudflare Pages, contradicting the in-repo ARCHITECTURE.md).