System overview
How the app works end-to-end
NXT is two user-facing surfaces (mobile app and marketing website) talking to one shared backend. Everything is TypeScript. Everything lives in one monorepo.
System diagram
The three surfaces
Mobile app (apps/mobile) — Expo SDK 55 + React Native. Ships as a real native iOS + Android app from the same source. Discover rails, college details, saved lists, quizzes, profile, notifications all live here. The mobile app talks directly to Convex; there is no separate API server in between.
Marketing website (apps/web) — Next.js 16 deployed on Vercel. Marketing-only: home, About pages, Contact, account-deletion request, privacy/terms. There is no signed-in surface on the web today; every product flow lives in the mobile app. The site's job is to drive App Store and Play Store installs.
Backend (packages/backend) — Convex. A single hosted service that replaces what would normally be three things: a database, an API server, and a real-time subscription system. When the app calls a Convex function, the function reads/writes the database; subscribed clients receive updates instantly. No middle tier.
How auth fits
WorkOS AuthKit handles identity. The mobile app sends users to WorkOS for sign-up/sign-in (email + password, magic link, Google OAuth on mobile). WorkOS returns a verified session. Convex trusts the session via its built-in WorkOS integration (convex/auth.config.ts). NXT does not store passwords.
This docs site is public — no auth, no sign-in. The <meta name="robots" content="noindex"> tag on every page prevents it from being indexed by search engines, so it doesn't compete with the marketing site for query terms. To gate the docs site behind WorkOS, install the docs-auth feature from create-nextbuild and re-upgrade.
How data fits
All factual college data is the federal College Scorecard API. NXT does not own or maintain the catalog; it reads from Scorecard with caching. See Data for details.
How the AI fits
AI is used in exactly one place: writing the 2–3 sentence "why this might be for you" blurb on each college card. The matching itself (Reach/Fit/Safety, the nine personalized rails, the Afford peek) is deterministic math, not AI. See AI and matching.
What happens when a user saves a school
Technical detail
Monorepo
- Tooling: pnpm workspaces + Turborepo.
pnpm checkruns typecheck + lint + format + workspace consistency + module boundaries + i18n in one gate. The pre-push hook runs the same set. - Module boundaries (
turbo.jsonboundaries.tags):webcannot importmobile,backendcannot import either,sharedcannot import any app code. Enforced bypnpm boundaries. - Catalog versions. All third-party deps are pinned in
pnpm-workspace.yamlcatalog:entries. No version drift across packages. - TypeScript strict mode across the workspace.
tsc --noEmitruns per package; no// @ts-ignoreallowed without a comment explaining why.
Mobile
- Expo SDK 55, React Native 0.74, React 19, Hermes.
- Routing: Expo Router v3 (file-system routing). Screens under
apps/mobile/app/. - State: Convex
useQuery/useMutationfor server data;zustandfor client state;@tanstack/react-formfor forms. - Styling: Tailwind v4 via Uniwind for React Native. Same tokens as web.
- Lists:
@shopify/flash-list(neverScrollView + .map()for unbounded lists). - Storage:
react-native-mmkvfor sync KV,expo-secure-storefor tokens. - Distribution: EAS Build → TestFlight (staging) → App Store (production). See Deployment.
Web
- Next.js 16 with App Router and the React Compiler.
proxy.ts(notmiddleware.ts— Next 16 split:proxy.tsruns on Node,middleware.tson Edge).- Tailwind v4.
- No
(authenticated)route group is wired today — the marketing-only stance is enforced by absence of authed routes, not by a feature flag. - Cache Components opt-in (Next 16
cacheComponents) is off. To enable, wrap every client page that readsuseQuery/useAction/useI18nat module load in<Suspense>at the data boundary.
Backend
- Convex deployment = one production + one dev. No staging tier; staging mobile builds point at production Convex (signed-in test accounts cover staging coverage).
- All convex functions use wrapped factories (
userQuery/userMutation/userAction) that pre-resolvectx.userfrom the verified WorkOS session. Rawquery()/mutation()is forbidden by lint. - Errors are constructed via factories in
convex/lib/errors.ts(base) orconvex/lib/errors-<feature>.ts. Each carriescode+ Englishmessage+messageKeyso the frontend can i18n-translate. ESLint rulecustom/no-raw-convex-throwprevents rawthrow new ConvexError(...).
Shared packages
@app/data-contractholds canonical TypeScript types + zod schemas. Apps and backend import the same types — no re-declaration.@app/i18nholds one flatlocales/en.jsonshared by both apps. Every user-facing string flows through it.pnpm i18nextracts new keys;pnpm i18n:lintflags hardcoded user-facing strings. The full pipeline is in place — adding a second locale (e.g. Spanish) is a translation file, not engineering work.@app/coreholds design tokens (one generation script writestokens.cssfor web +global.cssfor mobile), brand metadata, and the env helper.
Communication model
The mobile app uses Convex's React hooks. useQuery opens a live subscription — when the underlying data changes, every subscribed client gets the new value instantly. useMutation calls a Convex function; the response includes the mutation's effects.
The web app uses the same hooks but is read-only on a marketing-only surface.
There is no REST API, no GraphQL endpoint, no message queue, no separate worker process. Convex covers query, mutation, action (long-running with side effects), workflow (@convex-dev/workflow), and cron in one runtime.
Why no shared API tier
A separate API service would add a deploy target, a deploy step, a cold-start path, and a typing seam. Convex eliminates the need: types flow end-to-end (@app/data-contract), subscriptions arrive for free, and a single deploy ships database + functions + crons together. The trade-off is vendor lock-in on Convex — accepted explicitly. See Engineering conventions.