Local setup
How to run the app locally and ship a release
Prereqs
| Tool | Version | Notes |
|---|---|---|
| Node.js | 22.x (LTS) | Match .nvmrc |
| pnpm | 9.x or later | corepack enable |
| Xcode | 16+ | iOS builds via EAS; local sim via Xcode |
| Android Studio + SDK | latest | For Android sim / device |
| EAS CLI | latest | pnpm add -g eas-cli or via npx |
| Convex CLI | bundled in deps | pnpm --filter @app/backend dev invokes it |
First-time setup
# 1. Clone
git clone git@github.com-nxt:wearenxt/nxt-app.git
cd nxt-app
# 2. Install dependencies (workspace install)
pnpm install
# 3. Pull keys (gitignored files) from Bitwarden into:
# apps/mobile/keys/AuthKey_*.p8
# apps/mobile/keys/google-service-account.json
# apps/mobile/google-services.json
# 4. Copy env templates and fill from Bitwarden
cp .env.example .env.local
cp apps/web/.env.example apps/web/.env.local
cp apps/mobile/.env.example apps/mobile/.env.local
cp apps/docs/.env.example apps/docs/.env.local
# 5. Verify everything builds
pnpm checkpnpm check runs: typecheck + lint + format + workspace consistency (sherif) + module boundaries + i18n key check. Same gate the pre-push hook runs.
Run the app locally
Backend (Convex)
# Push convex code to your dev deployment; tail logs.
# First run prompts for deployment selection.
pnpm dev:backendThis must be running before mobile or web can talk to a live backend.
Mobile (iOS sim)
# In a second terminal, after backend is up:
pnpm ios
# Or Android:
pnpm androidThe first launch triggers an Expo dev client build (~3 min). Subsequent runs reuse the build and hot-reload JS.
Dev builds only. Expo Go cannot run native modules used in this app (WorkOS, MMKV, FlashList). Always use the dev client.
Web (marketing site)
pnpm dev:webOpens at http://localhost:3000. Marketing site only — there is no signed-in surface on web today.
Docs (this site)
pnpm --filter @app/docs devOpens at http://localhost:3001. Public — no sign-in required.
All at once
pnpm dev:allRuns backend + web + mobile in parallel via Turbo. Heavy on a laptop; use one-at-a-time during normal development.
Deploy
Mobile
# TestFlight (iOS + Android internal track)
pnpm build:staging --platform ios
pnpm submit:staging:ios
# Production App Store
pnpm build:prod --platform ios
pnpm submit:ios
# JS-only OTA update (no rebuild)
pnpm update:staging
pnpm update:prodWeb + docs
Push to main. Vercel auto-deploys.
Backend
pnpm --filter @app/backend deployRuns from the operator's machine today. See Deployment for moving this to CI.
Required environment variables (quick reference)
Repo root .env.example is the canonical list. Minimum to run mobile locally:
# WorkOS (same values for web + mobile; differ by deployment)
WORKOS_CLIENT_ID=
WORKOS_API_KEY=
WORKOS_COOKIE_PASSWORD=
WORKOS_REDIRECT_URI=http://localhost:3000/api/auth/callback
NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/api/auth/callback
# Convex (filled when `pnpm dev:backend` selects a deployment)
NEXT_PUBLIC_CONVEX_URL=
CONVEX_DEPLOYMENT=
EXPO_PUBLIC_CONVEX_URL=
EXPO_PUBLIC_WORKOS_CLIENT_ID=Server-side secrets (OpenAI, Scorecard, Serper, logo.dev, PostHog, WorkOS API key) live on the Convex deployment via npx convex env set. They are not in .env.local.
Common scripts
| Script | What it does |
|---|---|
pnpm check | Full pre-merge gate (typecheck + lint + format + boundaries + i18n) |
pnpm lint | ESLint across workspace |
pnpm format | oxfmt + prettier |
pnpm test | Vitest across all packages |
pnpm test:backend | Convex-test only |
pnpm i18n | Extract i18n keys from source into packages/i18n/locales/en.json |
pnpm i18n:lint | Flag hardcoded user-facing strings |
pnpm i18n:rename old.key new.key | Rename an i18n key across the codebase |
pnpm lint:dead | knip — find unused files/exports/deps (NOT in check) |
pnpm boundaries | Module boundary check (web ↛ mobile, etc.) |
pnpm doctor | One-shot health check of the workspace |
pnpm expo:fix | Repair common Expo native dep mismatches |
Never call vitest, jest, or playwright directly. Always pnpm scripts.
Known limitations
- No staging Convex. Both
stagingandproductionmobile builds point at production Convex. See Deployment. - No CI deploys. Convex production deploy runs from the operator's machine. GitHub Actions setup is a one-day task.
- Web is marketing-only. No signed-in product surface on the web; everything is mobile-first.
- English only. i18n catalogs are wired but only
en.jsonis filled. Adding Spanish is a translation pass, not engineering work. - No Sentry today. Wired in but DSN is blank. Turn on by setting
NEXT_PUBLIC_SENTRY_DSN+EXPO_PUBLIC_SENTRY_DSN. - No R2 / Resend in production paths. Both wired but inactive.
See Status for the full deferred-and-deliberate list.
Where to look next
- Recommended next priorities: Status — turn on Sentry, decide on Convex outbound backups, decide on CI deploys, finish the aggregate-component migration.
Where to ask questions
- Backend internals: read
packages/backend/CLAUDE.md— it points to Convex's own AI guidelines for anyone using AI coding tools. - App-wide conventions:
CLAUDE.md/AGENTS.mdat repo root. - Operational tasks:
.runbooks/(categorize a school, backfill college data, invalidate college cache).
Technical detail
Why pnpm check instead of pnpm test
check is the gate that prevents broken merges. Tests are a separate concern (correctness, not consistency). The pre-push hook runs check. CI runs check + test.
How i18n is enforced
pnpm i18n:lint is a custom rule that scans source for hardcoded user-facing strings (JSX text nodes, Text children, alert messages). It allow-lists strings that are clearly internal (log messages, dev-only console output). The custom/no-namespaced-i18n rule enforces no-arg getTranslations() / useTranslations() — namespaced calls corrupt the i18next-cli extract path.
Why monorepo
End-to-end types. The same College type flows from colleges.ts in convex, through @app/data-contract, into mobile + web React components. Breaking the schema means a typecheck error before runtime — not a deploy-time surprise.