NXT

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

Scroll to zoom · drag to pan · Esc to close

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

Scroll to zoom · drag to pan · Esc to close

Technical detail

Monorepo

  • Tooling: pnpm workspaces + Turborepo. pnpm check runs typecheck + lint + format + workspace consistency + module boundaries + i18n in one gate. The pre-push hook runs the same set.
  • Module boundaries (turbo.json boundaries.tags): web cannot import mobile, backend cannot import either, shared cannot import any app code. Enforced by pnpm boundaries.
  • Catalog versions. All third-party deps are pinned in pnpm-workspace.yaml catalog: entries. No version drift across packages.
  • TypeScript strict mode across the workspace. tsc --noEmit runs per package; no // @ts-ignore allowed without a comment explaining why.

Mobile

Web

  • Next.js 16 with App Router and the React Compiler.
  • proxy.ts (not middleware.ts — Next 16 split: proxy.ts runs on Node, middleware.ts on 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 reads useQuery / useAction / useI18n at 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-resolve ctx.user from the verified WorkOS session. Raw query() / mutation() is forbidden by lint.
  • Errors are constructed via factories in convex/lib/errors.ts (base) or convex/lib/errors-<feature>.ts. Each carries code + English message + messageKey so the frontend can i18n-translate. ESLint rule custom/no-raw-convex-throw prevents raw throw new ConvexError(...).

Shared packages

  • @app/data-contract holds canonical TypeScript types + zod schemas. Apps and backend import the same types — no re-declaration.
  • @app/i18n holds one flat locales/en.json shared by both apps. Every user-facing string flows through it. pnpm i18n extracts new keys; pnpm i18n:lint flags 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/core holds design tokens (one generation script writes tokens.css for web + global.css for 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.

On this page