OctoWiki

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

AppDomainFrameworkDeploy targetLocal dev
grasshopperclaim.octopuscards.io (+ sandbox-claim.*)Hono 4 + JSX, Drizzle + D1Cloudflare Workers (wrangler deploy) via GHAbun run dev (:8787)
clientapp.octopuscards.io (+ sandbox-app.*)Next.js 15.5 (standalone), React 19, MUI 7systemd Node on the VMsbun run dev (:3001)
octopus-websiteoctopuscards.io, www.*Next.js 16 + Fumadocs, static exportCloudflare Pages via GHAbun run dev (:3000)
octopus-docsdeveloper.octopuscards.ioNext.js 16 + Fumadocs + Mermaid, static exportCloudflare Pages via GHAbun 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-balboamocky-balboa.octopuscards.ioHono 4, Drizzle + D1Cloudflare Workers via GHAbun 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-workershttps://otel.octopuscards.io/v1/traces.
  • Talks to the Go backend via OCTOPUS_API_URL using OCTOPUS_CLIENT_USERNAME/PASSWORD, caching the Octopus JWT in the octopus_token KV to reduce /auth/login fan-out from cold isolates.
  • D1: binding DB, database grasshopper (d99ec4a1-5833-48ca-8eb6-49568a13d5fd); sandbox grasshopper_sandbox (d0f8b860-…). Migrations in drizzle/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 --minify

Deploy: GHA deploy-grasshopper.yml on push under frontend/grasshopper/**bun installapply remote D1 migrationswrangler 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 to output: 'export'; dynamic routes like /products/[id] need SSR.
  • Deploy = systemd Node standalone (NOT Cloudflare). Built with bun, run with Node. deploy/update-octopus-client.sh builds and copies .next/standalone + .next/static + public into /opt/octopus-client, served by octopus-client.service/usr/bin/node /opt/octopus-client/server.js on :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.ymlwrangler 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.ymlwrangler 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.iooctopus-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.

On this page