NXT

Deployment

Where each piece ships and how

Three pieces ship; each on its own cadence.

Mobile app (iOS + Android)

Built and distributed via EAS — Expo Application Services, the official build/release system for Expo apps.

Profiles

Configuration: apps/mobile/eas.json. Four profiles:

ProfilePurposeDistribution
developmentLocal dev builds (current operator's machine)internal
previewInternal-share builds for stakeholder reviewinternal
stagingTestFlight + Play internal trackstore, staging channel
productionPublic App Store + Play Storestore, production channel

Build + submit commands

# Staging (TestFlight)
pnpm build:staging --platform ios
pnpm submit:staging:ios

# Production
pnpm build:prod --platform ios
pnpm submit:ios

EAS handles compiling the native binaries. Apple requires a Mac to build iOS; EAS provides one. After EAS finishes a build, the same toolchain submits to TestFlight or Play internal track using API keys configured in eas.json.

Over-the-air (OTA) updates

For JavaScript-only fixes between full builds, publish OTA via Expo Updates:

pnpm update:staging   # OTA to staging channel
pnpm update:prod      # OTA to production channel

Existing installed apps pull the JS update on next launch — no App Store review. Native changes (new permissions, new native modules, config plugin changes) still require a full rebuild.

Store-side workflows

Two CLIs handle work EAS doesn't cover:

  • ascApp Store Connect (TestFlight beta groups, metadata, screenshots, IAP, reviewer responses, device registration).
  • gplayGoogle Play Console (listing updates, screenshots, staged rollouts, review responses).

Wrapped via pnpm asc / pnpm gplay. Full setup at apps/mobile/PUBLISHING.md. Both use API keys only — no interactive 2FA via web console.

Credentials

  • Apple ASC API key: apps/mobile/keys/AuthKey_68Y3V9WSG7.p8 (gitignored). Configured in eas.json submit profiles via ascApiKeyIssuerId + ascApiKeyId.
  • Google Play service account: apps/mobile/keys/google-service-account.json (gitignored). Configured in eas.json submit profiles.
  • Firebase config (Android push): apps/mobile/google-services.json (gitignored).

All three files live on the operator's machine. Bitwarden holds copies. No file lives in GitHub.

Marketing website

Vercel. Repo linked to a Vercel project. Every push to main triggers an automatic production deploy in roughly 60–90 seconds. Pull requests get automatic preview URLs. There is no manual deploy step.

  • Domain: wearenxt.com (DNS pointed at Vercel).
  • Analytics + speed monitoring: Vercel Analytics + Vercel Speed Insights wired in (@vercel/analytics, @vercel/speed-insights). Free tier; well within limits.
  • Env vars: managed in the Vercel project's Environment Variables settings. The full required list lives in apps/web/.env.example.

What ships to web

apps/web/app/(marketing)/ — public marketing routes (home, About, Contact, privacy/terms, account-deletion request). There is no (authenticated) route group today.

apps/web/proxy.ts runs middleware on the Node runtime. If a future change needs Edge runtime, that goes in middleware.ts — Next 16 splits the two by file name.

Backend (Convex)

Convex Cloud. Fully managed.

# Dev — push code to dev deployment on save
pnpm --filter @app/backend dev

# Production — deploy
pnpm --filter @app/backend deploy
# or:
cd packages/backend && npx convex deploy

There is no "deploy" run manually during normal development. The production deploy runs from a local machine today. Moving it to GitHub Actions is a one-day task; see Roadmap.

Convex auto-scales reads, writes, subscriptions, and crons. No scaling configuration to manage. No database backups to script (snapshots are automatic; see Data).

Docs site (this site)

Vercel. This Fumadocs app deploys as a separate Vercel project. Public — no auth gate. <meta name="robots" content="noindex"> keeps it out of search engines so it doesn't compete with the marketing site for query terms.

Deploy: same push-to-main model as the marketing site, but a separate Vercel project.

Staging vs production

  • Mobile: staging = TestFlight, production = App Store. Two separate Expo channels.
  • Web (marketing): Vercel preview URLs per-PR; production = wearenxt.com. No long-lived staging URL.
  • Backend: no separate staging Convex deployment. Both staging and production mobile builds point at production Convex. Signed-in test accounts provide staging coverage.
  • Docs (this site): Vercel preview per-PR + a single production URL. Public, noindex.

Skipping a dedicated staging Convex was deliberate. A staging backend doubles operator burden (two sets of crons firing, two sets of credentials, two sets of webhook endpoints) without catching anything the dev deployment doesn't already catch. Re-add later if the cost of a bad staging→prod surprise exceeds the cost of running a parallel backend.

Environment variables

A full inventory with comments lives in .env.example at the repo root. Server-side secrets that touch Convex are set via npx convex env set <key> <value> against the production deployment.

See Credentials for the canonical service-by-service list.

Technical detail

EAS build caches

EAS caches pnpm install output between builds. A pnpm-lock.yaml change invalidates the cache. Custom config plugins (app.config.ts) also invalidate. Average build time after warm cache: ~6 minutes iOS, ~5 minutes Android. Cold cache: add ~4 minutes per platform.

Vercel previews and Convex

Web previews on Vercel use the production Convex deployment. Read-only marketing site; no risk of preview traffic polluting production data. If a future change adds authenticated web routes, branch-scoped Convex preview deployments need to be wired (Convex supports it via the Convex Vercel integration).

Next 16 specifics

  • apps/web/proxy.ts = Node runtime. middleware.ts = Edge runtime. Don't use Edge-only APIs in proxy.ts.
  • params / searchParams / cookies() / headers() are async. Always await.
  • redirect() / notFound() throw internally — never wrap in try/catch.
  • cacheComponents is OFF. Opt in by wrapping every client page that reads useQuery / useAction / useI18n at module load in <Suspense> at the data boundary.
  • Use connection() before process.env reads in dynamic code (replaces unstable_noStore).
  • Use after() from next/server for fire-and-forget post-response work (analytics writes, audit logs).

Convex deploys are transactional

npx convex deploy is atomic. Either the whole deploy lands (functions, schema migration, cron registration) or none of it does. There is no half-deployed state to recover from. The trade-off: a schema change incompatible with in-flight traffic must be done in two deploys (add → backfill → switch reader → remove). This is what convex/migrations.ts is for.

On this page