Companion to CLAUDE.md. Deeper reference material for agents implementing non-trivial features. Read CLAUDE.md first — this doc assumes you've absorbed it.
Stadium has a deliberate split:
-
API runtime = Supabase. The live HTTP API reads and writes Supabase. Entry points:
server/db.js— builds the Supabase client fromSUPABASE_URL+SUPABASE_SERVICE_ROLE_KEY.server/api/repositories/project.repository.js— the single canonical access layer. Controllers and services go through it. All snake_case ↔ camelCase translation happens in itstransformProjectfunction.- SQL schema lives in
supabase/migrations/.
-
Offline tooling = MongoDB via Mongoose. Not in the request path at all. Used by
server/scripts/*.jsfor bulk imports, backfills, and reports against a local Mongo. Schema:server/models/Project.js,server/models/MultisigTransaction.js.
These don't talk to each other at runtime. If you catch yourself reaching across layers (Mongoose in server/api/, or Supabase in server/scripts/), stop and re-check which layer you should be in.
React component
└─ calls function in client/src/lib/api.ts
└─ fetch() → VITE_API_BASE_URL (dev: http://localhost:2000/api)
└─ Express route in server/api/routes/m2-program.routes.js
└─ middleware (logging, auth if admin)
└─ controller in server/api/controllers/project.controller.js
└─ service in server/api/services/*.js (business logic)
└─ repository in server/api/repositories/project.repository.js
└─ Supabase client (server/db.js)
└─ Postgres (Supabase)
The repository owns all .from('projects').select(...) style calls. A controller should never import the Supabase client directly.
notification.service.js is the dispatcher. Call notify(walletAddress, eventType, sourceId, payload) to write an audit row to the notifications table and, for eligible recipients (contact row present, email set, notificationsEnabled not false), send a transactional email via Resend. The four valid event types are application_accepted, application_rejected, m2_approved, and m2_changes_requested. To disable email locally, unset RESEND_API_KEY — sends become failed / provider_not_configured but the server still runs. Under NODE_ENV=test a mock transport (__tests__/mock-resend.js) is used automatically and no network call fires.
Client side (client/src/lib/siwsUtils.ts + AdminPage.tsx):
- User connects Polkadot-JS extension.
- Client fetches a nonce from the server.
- User signs a SIWS message (domain-bound, nonce-bound).
- Client sends signed payload in the
x-siws-authheader (see CORS config inserver/server.js) to a protected endpoint.
Server side (server/api/middleware/auth.middleware.js):
- Parses the SIWS message.
- Verifies the signature against the claimed address (on-chain, or via sr25519/ed25519 verify).
- Checks the address is in
ADMIN_WALLETS(or is a team member for routes that userequireTeamMemberOrAdmin). - Attaches auth context to the request and calls
next(), else returns 401/403.
Canonical test: server/api/middleware/__tests__/verify-onchain.test.js. Match its shape for new auth-related tests.
Do not write a parallel auth implementation. If you need a new admin-protected route, attach requireAdmin or requireTeamMemberOrAdmin and move on.
client/src/App.tsx uses react-router-dom v6. Dark mode is forced via useEffect in App.tsx. Do not add a theme toggle unless the plan explicitly requires it.
client/src/index.css defines CSS custom properties for the shadcn theme. Only touch variables, not raw hex values. Never add !important.
Config: server/vitest.config.js. Environment: node. Test pattern: **/__tests__/**/*.test.{js,ts} and **/tests/**/*.test.{js,ts}.
Run: cd server && npm test.
Writing a new server test:
// server/api/<area>/__tests__/<name>.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('<name>', () => {
it('<behavior>', async () => {
// arrange, act, assert
});
});Use vi.mock() for external deps (the Supabase client, polkadot-api). Prefer testing pure functions and the repository layer directly. When testing middleware, construct req/res/next manually per the style in verify-onchain.test.js.
Unit tests should not require a real Supabase connection — mock at the repository or client level.
There is no test runner configured on the client. Do not add one in a passing pass; open an issue if the feature really needs it.
Client verification is npm run build (tsc + Vite) and npm run lint. Both must pass before PR.
- Entry:
server.jsvianpm start. railway.json,nixpacks.toml,Dockerfilelive atserver/.- Runtime env vars in Railway dashboard:
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,ADMIN_WALLETS,EXPECTED_DOMAIN,NODE_ENV,NETWORK_ENV,AUTHORIZED_SIGNERS,CORS_ORIGIN. MONGO_URIis not a production runtime requirement — it's only needed if someone is runningserver/scripts/*.jslocally.- Never commit real values. Update
server/.env.examplewhen adding a new required var.
npm run buildoutput deployed by Vercel.- Env vars:
VITE_API_BASE_URL,VITE_ADMIN_ADDRESSES(comma-separated). - Preview deployments from PRs work normally.
- Preview env only:
VITE_USE_MOCK_DATA=trueserves fixtures instead of the API.VITE_USE_TEST_WALLET=trueactivates the//Alicetest-wallet harness so thestadium-testerSkill can exercise SIWS-gated flows. Alice's SS58-42 address (5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY) must also be inVITE_ADMIN_ADDRESSESon Preview for admin flows to pass the client gate. Production must leave both flags unset and must never include Alice's address inVITE_ADMIN_ADDRESSESor the server'sADMIN_WALLETS/AUTHORIZED_SIGNERS— the //Alice mnemonic is public.
Do not modify Railway/Vercel config from an issue unless the issue explicitly calls for it.
- Writing Mongoose queries in
server/api/**. The API is Supabase.server/models/Project.jsexists only for scripts. If you are tempted to import it from a controller or service, you're on the wrong layer. BYPASS_ADMIN_CHECK = trueshipped once inAdminPage.tsx. Full auth bypass. Alwaysfalse.- Duplicate toast hook — a stray
use-toast.tsalongsideuse-toast.tsxcaused Vite to pick the wrong one. Only.tsxexists now; do not create.ts. - Hardcoded hex colors with
!importantinindex.cssbroke dark mode. Use theme CSS variables. console.login production client — all stripped. Use the toast or error boundary instead.- Controllers bypassing the repository — any new query should land in
server/api/repositories/project.repository.js, not inline in a controller. - Client
npm test— there is no such script. Don't invent one and don't add it to CI expectations. - Supabase service-role key in client — never. The service-role key is server-only. The client talks to the server, not Supabase directly.
- Polkadot.js extension docs: https://polkadot.js.org/docs/extension/
- Talisman SIWS package:
@talismn/siws(used both client and server) - shadcn/ui: https://ui.shadcn.com/
- Supabase JS SDK: https://supabase.com/docs/reference/javascript/introduction
See also docs/API_DOCS.md, docs/DATA_SCHEMA.md, docs/PRODUCTION_DEPLOYMENT.md.