NXT

Local setup

How to run the app locally and ship a release

Prereqs

ToolVersionNotes
Node.js22.x (LTS)Match .nvmrc
pnpm9.x or latercorepack enable
Xcode16+iOS builds via EAS; local sim via Xcode
Android Studio + SDKlatestFor Android sim / device
EAS CLIlatestpnpm add -g eas-cli or via npx
Convex CLIbundled in depspnpm --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 check

pnpm 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:backend

This 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 android

The 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:web

Opens at http://localhost:3000. Marketing site only — there is no signed-in surface on web today.

Docs (this site)

pnpm --filter @app/docs dev

Opens at http://localhost:3001. Public — no sign-in required.

All at once

pnpm dev:all

Runs 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:prod

Web + docs

Push to main. Vercel auto-deploys.

Backend

pnpm --filter @app/backend deploy

Runs 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

ScriptWhat it does
pnpm checkFull pre-merge gate (typecheck + lint + format + boundaries + i18n)
pnpm lintESLint across workspace
pnpm formatoxfmt + prettier
pnpm testVitest across all packages
pnpm test:backendConvex-test only
pnpm i18nExtract i18n keys from source into packages/i18n/locales/en.json
pnpm i18n:lintFlag hardcoded user-facing strings
pnpm i18n:rename old.key new.keyRename an i18n key across the codebase
pnpm lint:deadknip — find unused files/exports/deps (NOT in check)
pnpm boundariesModule boundary check (web ↛ mobile, etc.)
pnpm doctorOne-shot health check of the workspace
pnpm expo:fixRepair common Expo native dep mismatches

Never call vitest, jest, or playwright directly. Always pnpm scripts.

Known limitations

  • No staging Convex. Both staging and production mobile 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.json is 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.md at 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.

On this page