# Service conventions - Stack: Vite + React 19 + TypeScript + React Router v8 in data mode (`createBrowserRouter`, SPA, no SSR), Node 22 for build only, vitest + Testing Library, nginx-unprivileged at runtime. - Do NOT rename npm scripts (`lint`, `test`, `format:check`, `build`): the shared CI composite actions call them. - Routes: table in `src/routes.tsx`, one component per file in `src/routes/`, loaders exported next to their component. - Runtime config: read only via `getConfig()` (`src/lib/config.ts`). A new browser-visible env var must be added to `VARS` in `docker-entrypoint.d/10-config.sh`, `AppConfig`, and the gitops Deployment. No secrets: config.js is public. - Backend calls: `apiGet()` in `src/lib/api.ts`, relative to `API_BASE_URL` (`/api` same-host ingress path, stripped before the backend). No CORS. - Serving contract: port 8080, `/healthz` 200, SPA fallback, immutable `/assets/`, no-cache `index.html`/`config.js` (`nginx/default.conf`). - Deploy: push to `main` -> Forgejo CI -> image -> gitops bump -> ArgoCD. Never `kubectl apply` by hand.