Platform Overview
The web platform (envirobot.yerielph.workers.dev) is a Next.js 15 site deployed as a single Cloudflare Worker. It documents the project, replays run data for judges, browses the firmware source, previews the CAD model, and gives the team a way to edit all of that content without a redeploy.
Stack
| Layer | Technology | Role |
|---|---|---|
| Framework | Next.js 15 (App Router) + TypeScript + Tailwind 4 | Pages, API routes, styling |
| Runtime | Cloudflare Workers, via @opennextjs/cloudflare (OpenNext) |
Hosting — not Cloudflare Pages, not Vercel |
| 3D rendering | @react-three/fiber + @react-three/drei (three.js) |
Arena replay (/viz), CAD viewer (/design) |
| STEP parsing | occt-import-js (WASM, client-side) |
Converts STEP/STP geometry to a renderable mesh in-browser |
| Charts | Chart.js + react-chartjs-2 |
NTU / moisture bar charts in the visualiser |
| Database | Supabase (Postgres) | Content metadata: docs_pages, code_files, design_files |
| Object storage | Cloudflare R2, via aws4fetch (SigV4) |
File bytes: firmware source, CAD models |
| Animation | GSAP + @gsap/react |
Landing page scroll effects |
Two datastores, two jobs: Supabase holds metadata (filenames, ordering, doc markdown), R2 holds bytes (file content, addressed by r2_key). Every content type follows the same shape: a DB row pointing at an R2 object.
Route Map
| Route | Purpose | Guide |
|---|---|---|
/ |
Landing page — project pitch, scroll narrative | — |
/docs |
This documentation site | — |
/viz |
Run data visualiser — 2D/3D arena replay + charts | Visualiser |
/code |
Public Arduino firmware source browser | Code Browser |
/design |
CAD model viewer | CAD Viewer |
/admin |
Content management console (auth required) | Content Admin |
/admin/login |
Admin sign-in | Content Admin |
Content Pipeline
Firmware source and this documentation both have a canonical source in the git repo — the CMS is a mirror, not the source of truth, so the site can never show something the codebase doesn't actually do:
arduino/envirobot/*.cpp, *.h, *.ino ──sync-code.mjs──▶ app/api/admin/seed-code/route.ts
(embeds redacted file contents)
lib/docs-content.ts ──sync-docs.mjs──▶ content/docs/*.mdx
(mirror only — the site reads
docs-content.ts directly)
| Script | Run after | What it does |
|---|---|---|
node scripts/sync-code.mjs |
Any firmware change | Regenerates app/api/admin/seed-code/route.ts from arduino/envirobot/. Nothing is redacted — WiFi is AP-only, so the firmware holds no private credentials |
node scripts/sync-docs.mjs |
Any lib/docs-content.ts edit |
Mirrors each entry to content/docs/{slug}.mdx on disk, so doc sources are readable outside the CMS too |
After sync-code.mjs, hit POST /api/admin/seed-code from the admin panel (or curl) once to push the embedded files into R2 + the code_files table — that's what /code actually serves. Docs don't need a seed step: /api/docs falls back to docs-content.ts directly whenever no docs_pages row exists for a slug.
Precedence trap: if a doc page is ever edited and saved from
/admin, that write creates adocs_pagesrow which then overridesdocs-content.tspermanently for that slug — editing the source file after that point silently does nothing until the DB row is deleted or re-saved. Check thedocs_pagestable before assuming adocs-content.tsedit will show up live.
Deploy
cd webapp
npm run deploy # opennextjs-cloudflare build && opennextjs-cloudflare deploy (Wrangler under the hood)
- Never
vercelor the Vercel CLI — Wrangler only. - Config:
wrangler.jsonc(worker nameenvirobot,nodejs_compatflag) +open-next.config.ts. - Do not add
export const runtime = 'edge'to any route — OpenNext runs Node runtime semantics inside workerd; edge exports break the build. npm run previewbuilds and runs the Worker locally before pushing.
Secrets
Set once via wrangler secret put <NAME>; mirrored locally in .dev.vars (gitignored):
| Secret | Used by |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Both Supabase clients |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Public client — browser-side login, RLS-scoped |
SUPABASE_SERVICE_ROLE_KEY |
Admin client — server-side only, bypasses RLS. Never send to the browser |
R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY |
R2 SigV4 signing (lib/r2.ts) |
R2_ACCOUNT_ID, R2_BUCKET |
R2 object URL construction |
Where Each Feature's Data Lives
| Feature | Supabase table | R2 prefix | Public API |
|---|---|---|---|
| Docs | docs_pages (slug, title, content, updated_at) |
— (markdown stored in Postgres, not R2) | GET /api/docs?slug= |
| Code Browser | code_files (filename, lang, r2_key, display_order) |
code/ |
GET /api/code, /api/code/content, /api/code/download-all |
| CAD Viewer | design_files (filename, r2_key, display_order) |
(admin-chosen key) | GET /api/design, /api/design/content |
All public GET routes are edge-cached (s-maxage, varying by content volatility — code content is cached immutable since r2_key is content-addressed by filename, not hash, so a changed file gets a new upload rather than mutating in place).