This file is the first thing any agent working on this repo should read. It is the contract for how features and fixes are shipped here. For deeper detail see docs/AGENT_GUIDE.md.
Stadium is the web app for the WebZero hackathon/incubator program on Polkadot. Three components live here:
client/— React 18 + Vite + TypeScript + Tailwind + shadcn/ui. Talks to the server viaclient/src/lib/api.ts. Admin auth via Polkadot-JS extension + SIWS (Sign-In With Substrate).server/— Express 5, ESM ("type": "module"). Tests via Vitest.hackathonia/— Rust ink! smart contracts (out of scope for most feature work — touch only if the issue explicitly asks for contract changes).
Data layers (important — two co-exist):
- API runtime = Supabase.
server/db.jsbuilds a Supabase client fromSUPABASE_URL+SUPABASE_SERVICE_ROLE_KEY.server/api/repositories/project.repository.jsis the canonical data access layer — every controller and service goes through it. New API code adds queries here. - Offline tooling = MongoDB via Mongoose.
server/scripts/*.js(seed-dev, migration, fix-bounty-amounts, list-winners-zero-paid, set-live-urls, set-m2-final-submissions, seed-m2-test-project) use Mongoose against a local Mongo for bulk imports, backfills, and reports.server/models/Project.jsis their schema. Not wired to any route. Never add a Mongoose query toserver/api/**.
Deployment: server → Railway, client → Vercel.
Default branch on origin: develop. Long-lived branches include main, develop, design-revamp. PRs normally target develop.
Always run from the subdir, not the repo root.
Client (cd client):
npm run dev— Vite dev servernpm run build—tsc && vite build(this is the typecheck; there is no separatetypecheckscript — don't invent one)npm run lint— ESLint with--max-warnings 0(warnings fail)- No test script — do not write one in this pass
Server (cd server):
npm run dev— nodemonnpm test—vitest runnpm start—node server.jsnpm run seed:dev,npm run db:migrate,npm run db:reset— local Mongo tooling (destructive; run only when asked)npm run verify:production,npm run verify:main-deployed,npm run deploy:all— operational
client/src/
pages/ AdminPage, HomePage, M2ProgramPage, ProjectDetailsPage, WinnersPage, NotFound
components/ shadcn/ui + feature components
hooks/ use-toast.tsx, use-mobile
lib/ api.ts, constants.ts, siwsUtils.ts, polkadot-config.ts, addressUtils.ts, paymentUtils.ts, projectUtils.ts, utils.ts
App.tsx routing (react-router-dom v6)
index.css theme variables (dark mode only)
server/
server.js entry — imports connectToSupabase from db.js
db.js Supabase client construction (live API data layer)
api/
routes/ *.routes.js (currently: m2-program.routes.js)
controllers/ *.controller.js
services/ business logic
repositories/ project.repository.js — Supabase queries, single source of truth for API data access
middleware/ auth.middleware.js (SIWS), logging.middleware.js
utils/
constants/
models/ Mongoose models (script-only, NOT used by routes): Project.js, MultisigTransaction.js
scripts/ offline data tooling (Mongo-backed; destructive — do not run)
tests/ standalone tests
vitest.config.js
supabase/ SQL migrations for the live Supabase DB
These have bitten us before. Do not regress them.
BYPASS_ADMIN_CHECKinclient/src/pages/AdminPage.tsxmust stayfalse. Wastrueonce and shipped — it's a full auth bypass.- Admin wallet list comes from env (
VITE_ADMIN_ADDRESSESon client,ADMIN_WALLETSon server). Never hardcode addresses. - Never add Mongoose queries to
server/api/**. The API is Supabase. If you find yourself reaching forserver/models/Project.jsoutsideserver/scripts/, stop — you're on the wrong layer. - Never add Supabase calls to
server/scripts/the other way either. Scripts are Mongo-backed; keep them that way. - Toast hook path:
@/hooks/use-toastresolves touse-toast.tsx, not.ts(Vite resolution quirk). There used to be a duplicate.ts— do not reintroduce it. - Dark mode is forced in
App.tsx. Don't add light-mode-only styles or hex colors with!importantinindex.css. - No
console.log/console.warn/console.errorin production client code. They were stripped deliberately. - Server is ESM — use
import, notrequire. - Every admin-protected server route must use middleware from
auth.middleware.js:requireAdminfor admin-only,requireTeamMemberOrAdminfor team+admin. Seeserver/api/middleware/__tests__/verify-onchain.test.jsfor the canonical test shape. - Client env vars must be prefixed
VITE_or Vite will not expose them.
Recipes for the common shapes. Reuse the listed utilities; do not create parallel versions.
New client page:
- Create
client/src/pages/YourPage.tsx. - Register the route in
client/src/App.tsx. - API calls go through
client/src/lib/api.ts— add a function there, don'tfetch()inline. - Admin-gated? Use the existing auth pattern from
AdminPage.tsx(Polkadot extension + SIWS, seeclient/src/lib/siwsUtils.ts).
New server endpoint:
- Add a handler to
server/api/controllers/project.controller.js(or a new controller following its pattern). - Add the route in
server/api/routes/*.routes.js;server.jsmounts route files. - If it mutates admin data: attach
requireAdminorrequireTeamMemberOrAdminfromauth.middleware.js. - Data access goes through
server/api/repositories/project.repository.js(Supabase). Extend it — don't query Supabase directly from a controller, and don't reach for Mongoose. - Write a Vitest test in
server/api/**/__tests__/*.test.jsorserver/tests/.
New admin-protected endpoint checklist:
- Route uses
requireAdminorrequireTeamMemberOrAdmin - Data access through
project.repository.js(Supabase) - Test covers both authorized and unauthorized cases
Database schema change (columns / tables):
- Add a SQL migration under
supabase/migrations/. - Update the transform function in
server/api/repositories/project.repository.js(snake_case ↔ camelCase). - Regenerate / apply in Supabase per
docs/PRODUCTION_DEPLOYMENT.md.
When you ship an issue, follow this loop. It is enforced by slash commands in .claude/commands/.
- Explore — read the issue, map affected files. Use the
stadium-explorersubagent for anything non-trivial. - Plan — produce a written plan: files to change, tests to add, invariants to respect. Post it for the user.
- Wait for approval — do not write code until the user (or a CODEOWNER in CI) approves the plan. This is non-negotiable.
- Implement — smallest diff that satisfies the issue. Reuse utilities. No scope creep.
- Verify — run
/pre-pr-check(server tests + client build + client lint), thenstadium-testeragainst the Vercel preview / local dev server using the issue's## Test scenarios. Both must pass. If the issue has zero scenarios, stop and ask the author to add them — no PR is opened without scenarios. If any scenario FAILs, return to the implementer — no PR on failures. The tester's report must be pasted into the PR body. - Draft PR — always open as draft targeting
develop. Link the issue. Summary + test plan +stadium-testerreport + any backlog entries created. - Iterate on review — when the reviewer leaves comments, use
/address-review <pr>to fetch them, classify (CODE / REPLY / REJECT), wait for user approval of the classification, then push fixes and reply. - Stop — never merge. A human CODEOWNER reviews and merges. The agent is never a CODEOWNER.
If any step fails, stop and report. Do not bypass with --no-verify, do not disable tests, do not force-push shared branches.
While working, you will notice things that are wrong or could be better but are out of scope for the current issue. Log them; do not silently fix them.
- Nits and small observations → append to
docs/improvement-backlog.mdusing the template there, via/log-improvement. - Real bugs or meaningful improvements → open a GitHub issue with the
claude-suggestedlabel via/promote-backlog(which asks you first). - The agent never creates issues without asking. The agent never merges.
ghCLI may or may not be installed locally. Slash commands check and fail with a clear message if it's missing.- Node: match CI; check
.nvmrcif present. - Server runtime needs
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,ADMIN_WALLETS,EXPECTED_DOMAIN,AUTHORIZED_SIGNERS,NETWORK_ENV. Seeserver/.env.example. - Local Mongo tooling (only if you're running
npm run seed:dev/db:*/list:winners-zero-paid) additionally needsMONGO_URIand a localmongosh. - Copy
client/.env.example→client/.envandserver/.env.example→server/.env. Never commit real secrets.
Every branch pushed to origin gets an automatic Vercel preview URL for the client. Previews run in mock mode — VITE_USE_MOCK_DATA=true must be set on the Preview environment in the Vercel dashboard (scope = Preview only, not Production). When this flag is on, client/src/lib/api.ts serves fixtures from src/lib/mockWinners.ts for reads and simulates writes in localStorage / in-memory mock objects. Production must leave the flag unset or false.
Use the preview URL for visual review before approving a /ship-issue PR. Real-API end-to-end testing happens in the Railway staging / production environment, not from a branch preview.
Subagents (.claude/agents/):
stadium-explorer— codebase search tuned to this layoutstadium-implementer— writes the code per the approved planstadium-reviewer— pre-PR check against repo invariants
Skills (.claude/skills/):
stadium-tester— drives a real headless Chromium against the Vercel preview / dev server and verifies each issue's## Test scenarios. Invoke as/stadium-tester <target-url> "<scenarios>". Auto-loads when the agent is verifying UI behavior.
Slash commands (.claude/commands/):
/ship-issue <number>— full workflow for a single issue (explore → plan → approve → implement → verify → tester → draft PR)/triage-issue <number>— plan only, posted as a comment/address-review <pr>— fetch PR review comments, classify, address, push, and reply/log-improvement <desc>— append to backlog/promote-backlog— convert backlog entries to GH issues (asks first)/pre-pr-check— run server tests + client build + client lint/verify-tester— one-shot health check that thestadium-testerSkill is wired up correctly (Playwright + Chromium installed, runner reachable, prod guard intact)
The tester is project-scoped: it lives at .claude/skills/stadium-tester/ and uses local Playwright (installed into client/node_modules via bash .claude/skills/stadium-tester/setup.sh). First run on a fresh machine downloads Chromium (~150MB).