Fennec Local Development
How to bring fennec up locally for development. Two halves: the Vite SPA and the embedded Strapi 5 backend.
Prerequisites
- Node.js 22 (matches
.github/workflows/fennec.ymlNODE_VERSION). - pnpm 10.26.0 (matches the workflow
PNPM_VERSION). The repo has both apnpm-lock.yamland a straypackage-lock.json— always use pnpm perapps/fennec/CLAUDE.md. - Docker (for the Strapi backend, Postgres, and Redis via the embedded
docker-compose.yml/docker-compose-local.ymlunderapps/fennec/). - A Clerk dev instance with a publishable key.
- Network access to a running brain dev instance (or stub).
Initial setup
cd apps/fennecpnpm install --frozen-lockfileCreate apps/fennec/.env (or copy from a teammate’s known-good values).
Required values (see fennec-env.md for the full table):
REACT_APP_CLERK_PUBLISHABLE_KEY=pk_test_...REACT_APP_BACKEND_URL=http://localhost:9950REACT_APP_BRAIN_URL=http://localhost:3001REACT_APP_R2BUCKET_NAME=tenderloin-devREACT_APP_FENNEC_API_TOKEN=<shared dev token>Important: Vite is configured with envPrefix: "REACT_APP_"
(apps/fennec/vite.config.ts). Variables without that prefix are not
exposed to the SPA.
Run the SPA
cd apps/fennecpnpm dev # alias for `vite` — runs on http://localhost:3000Vite serves on 0.0.0.0:3000 (server.host: true) so containers and other
devices on the LAN can reach it. The dev server has no proxy block —
upstream URLs come straight from REACT_APP_BACKEND_URL and
REACT_APP_BRAIN_URL, so make sure CORS on those upstreams allows
http://localhost:3000.
The repo’s top-level make dev-up-d brings up the full stack including
fennec; use it when you need the whole platform. Use the per-app pnpm dev
when iterating on fennec only.
Run the Strapi backend (mock / embedded)
The “backend” the SPA talks to (REACT_APP_BACKEND_URL) is a Strapi 5
project at apps/fennec/backend/. It is managed via docker-compose:
cd apps/fennecdocker compose -f docker-compose-local.yml up -dThis brings up Strapi (default :9950), Postgres, and Redis with values
from apps/fennec/backend/.env.
Mocking just brain — if you do not need real generations data, point
REACT_APP_BRAIN_URL at a local mock server. There is no first-class mock
backend bundled with fennec today (TODO(@law): confirm with team if a
preferred mock exists; otherwise standard practice is to run brain locally
or use a fixture-based mock service).
Quality gates
Match what CI runs:
pnpm lint # ESLint, --max-warnings 0pnpm tsc # tsc --noEmitBoth must pass before pushing. Additional helpful commands:
pnpm lint:fix # autofix lintpnpm format:all # Prettier across srcpnpm format:staged # Prettier on staged files onlypnpm knip # dead-code detectionpnpm knip:exports # unused exportspnpm build # production build (vite build → dist/)pnpm preview # serve the production build locallyAuth in local dev
Sign-in uses Clerk. Two practical setups:
- Real Clerk dev instance. Set
REACT_APP_CLERK_PUBLISHABLE_KEYto the dev publishable key. Brain must use the matchingCLERK_SECRET_KEYso tokens issued by your SPA are accepted. - Shared team Clerk dev. If the team uses a shared dev Clerk environment, copy that key from your teammate or 1Password.
Roles are enforced both by PrivateRoute
(apps/fennec/src/components/PrivateRoute.tsx) and brain. To reach
admin-only pages locally, your brain users row must have role set to
ADMIN or ROOT — see apps/fennec/src/lib/auth/roles.ts.
Common local issues
| Symptom | Fix |
|---|---|
apiConfig.brain is undefined in DevTools | REACT_APP_BRAIN_URL missing — set it and restart pnpm dev (env values are read at startup) |
CORS errors hitting localhost:3001 | Brain CORS allowlist missing http://localhost:3000 |
| Cookies not stuck after sign-in | withCredentials requires brain to send Access-Control-Allow-Credentials: true with explicit origin |
| Port 3000 already in use | Kill the holder, or run vite --port 3001 and update Clerk allowed origins |
| Strapi container won’t start | docker compose down -v then bring it back up; check apps/fennec/backend/.env for typos |
| ESLint flagging files you didn’t change | Pull latest base configs (eslint.config.base*.mjs at repo root) |
pnpm install slow / wrong lockfile | Two lockfiles exist — never use npm install; always pnpm install --frozen-lockfile |
Day-to-day workflow
pnpm devinapps/fennec/.- Code changes hot-reload via Vite.
- Before committing:
pnpm lint && pnpm tsc. - Open a PR;
.github/workflows/fennec.ymlre-runs lint + typecheck. - After merge, Railway deploys (see
fennec-runbook.md).