← All docs

Deployment

This monorepo is the Twentyfirst Media Group product ecosystem — six deployable web apps plus shared packages. See .design/README.md for brand context and the root README for the full property map.

AppDomainWho hostsDatabase
Chat workspace (apps/chat.twentyfirst.ai)chat.twentyfirst.ai (or customer domain)Client (on-prem or private cloud)DATABASE_URL
Control plane (apps/twentyfirst.ai)twentyfirst.aiTwentyfirstCONTROL_DATABASE_URL
Product engineering (apps/twentyfirst.dev)twentyfirst.devTwentyfirstDATABASE_URL (Drizzle, optional)
AI marketplace (apps/platform.twentyfirst.ai)platform.twentyfirst.aiTwentyfirstDATABASE_URL (Prisma)
Corporate media (apps/twentyfirst.media)twentyfirst.mediaTwentyfirstDATABASE_URL (Drizzle)
AI academy (apps/twentyfirst.academy)twentyfirst.academyTwentyfirstDATABASE_URL (Drizzle)

Prerequisites

  • Node.js 22.13+ (see .nvmrc; CI runs on Node 22)
  • PostgreSQL 18 + pgvector (chat workspace)
  • PostgreSQL 18 (control plane, platform, media)
  • OpenAI-compatible model endpoint — see model-servers.md (chat workspace)
  • Object storage: local (dev) or S3-compatible (production)
  • Redis (chat workspace production — TCP rate limits; default REDIS_URL=redis://127.0.0.1:6379)

Environment files

Copy and configure:

  • apps/chat.twentyfirst.ai/.env.example → apps/chat.twentyfirst.ai/.env
  • apps/twentyfirst.ai/.env.example → apps/twentyfirst.ai/.env
  • apps/twentyfirst.dev/.env.example → apps/twentyfirst.dev/.env
  • apps/platform.twentyfirst.ai/.env.example → apps/platform.twentyfirst.ai/.env
  • apps/twentyfirst.media/.env.example → apps/twentyfirst.media/.env
  • apps/twentyfirst.academy/.env.example → apps/twentyfirst.academy/.env

CONTROL_PLANE_SIGNING_KEY must match on chat workspace and control plane (license verify + config signing).

twentyfirst.dev needs the AWS SES credentials of twentyfirst.media (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, EMAIL_FROM, LEAD_NOTIFY_EMAIL) so the PHP2Shell website check can email every check to business@ — see php2shell-landing.md. Without them the site works, but no lead emails are sent.

For CI/Docker builds without secrets: SKIP_ENV_VALIDATION=1.

Build

npm ci
npm run lint
npm run build

Run individually:

npm run start -w @repo/chat.twentyfirst.ai   # port 3027
npm run start -w @repo/twentyfirst.ai        # port 3028
npm run start -w @repo/twentyfirst.dev       # port 3029
npm run start -w @repo/platform.twentyfirst.ai  # port 3030
npm run start -w @repo/twentyfirst.media     # port 3031
npm run start -w @repo/twentyfirst.academy   # port 3032

Production server (PM2)

All six apps run on the production server from /var/www/twentyfirst under PM2, defined in ecosystem.config.cjs (fork mode):

PM2 nameApp folderPort
chat.twentyfirst.aiapps/chat.twentyfirst.ai3027
twentyfirst.ai-controlapps/twentyfirst.ai3028
dev.twentyfirst.aiapps/twentyfirst.dev3029
platform.twentyfirst.aiapps/platform.twentyfirst.ai3030
twentyfirst.media-nextapps/twentyfirst.media3031
academy.twentyfirst.aiapps/twentyfirst.academy3032

Each entry runs scripts/start-app.js <app-dir> <port>, which:

  • loads apps/<app>/.env (stays on the server, never committed); a PORT there wins over the port argument,
  • resolves Next.js from the app folder (require.resolve("next/dist/bin/next", { paths: [appDir] })) and logs the version, e.g. [start-app] twentyfirst.media: next@16.4.3 …,
  • runs next start and forwards SIGINT/SIGTERM.

All apps share one Next.js version in the root node_modules; CI fails if a nested copy appears.

Deploy

scripts/deploy.sh <app|all>    # e.g. scripts/deploy.sh platform.twentyfirst.ai

Runs git pull --ff-only, npm ci (install scripts enabled, dev dependencies included), then per app prisma generate (+ prisma migrate deploy if the app has Prisma migrations), the production build and — only if all of that succeeded — pm2 restart <name> --update-env. A failed app keeps running its previous build and the script exits non-zero. No pkill/pgrep.

Build-time public values set by the deploy script (Next.js inlines NEXT_PUBLIC_* at build time):

AppAlways setDefault if not in the app's .env
platformNEXT_PUBLIC_DOMAIN=platform.twentyfirst.ai, NEXT_PUBLIC_COMING_SOON=false–
chat–NEXT_PUBLIC_APP_URL=https://chat.twentyfirst.ai (optional NEXT_PUBLIC_BASE_DOMAIN from .env)
control–NEXT_PUBLIC_CONTROL_URL=https://twentyfirst.ai
dev–NEXT_PUBLIC_APP_URL=https://dev.twentyfirst.ai
academy–NEXT_PUBLIC_APP_URL=https://academy.twentyfirst.ai
media–– (tracking IDs such as NEXT_PUBLIC_GA4_MEASUREMENT_ID, NEXT_PUBLIC_GOOGLE_ADS_*, NEXT_PUBLIC_MICROSOFT_CLARITY_ID, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, NEXT_PUBLIC_EMERGENCY_PHONE come from its .env)

NODE_ENV=production is always set for builds. npm ci needs access to cdn.sheetjs.com (official source of xlsx).

Email

All apps send mail only through the Amazon SES API (AWS SDK v3): credentials AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY, region AWS_SES_REGION (falls back to AWS_REGION, default eu-central-1), sender EMAIL_FROM. There is no SMTP transport. The platform scam checker still reads its inbox via IMAP.

Database

Chat workspace

npm run db:push -w @repo/chat.twentyfirst.ai
npm run db:seed -w @repo/chat.twentyfirst.ai

Optional RLS: psql $DATABASE_URL -f apps/chat.twentyfirst.ai/drizzle/rls.sql

Control plane

npm run db:control:push
npm run db:seed -w @repo/twentyfirst.ai

AI marketplace (Prisma)

npm run db:platform:migrate

Corporate media (Drizzle)

npm run db:media:push
npm run db:media:seed

AI academy (Drizzle)

npm run db:academy:push
npm run db:academy:seed

twentyfirst.dev (Drizzle, optional) — PHP2Shell check results, report emails, double opt-ins and follow-ups. Without DATABASE_URL the website check still works; report emails and follow-ups are off.

npm run db:dev:push

Optional: sync Stripe test products from seeded plans — npm run stripe:sync -w @repo/twentyfirst.academy (requires Stripe keys in .env).

Docker

docker compose up --build

Local databases only (no app containers for platform/media yet):

docker compose up -d workspace-db control-db platform-db media-db academy-db

See docker-compose.yml for Postgres services and optional chat/control app images.

Chat workspace (client install)

  1. Deploy apps/chat.twentyfirst.ai in the client VPC (or on customer infrastructure).
  2. Pair to twentyfirst.ai with a one-time INSTANCE_PAIRING_TOKEN, or use air-gapped license + config import.
  3. Point MODEL_BASE_URL at the client's model endpoint (on-prem GPU, private cloud, etc.).
  4. Point REDIS_URL at the instance Redis (default redis://127.0.0.1:6379 for a local daemon). No Upstash REST URL or token is required.
  5. Schedule POST /api/cron/sync (Bearer CRON_SECRET) for config pull and usage push.

Health: GET /api/health

Control plane (Twentyfirst)

  1. Deploy apps/twentyfirst.ai on your infrastructure.
  2. Restrict /ops to operator networks or SSO.
  3. Provision customers and instances via the ops UI.
  4. Instance API (/api/v1/*) is machine-facing — protect with instance API keys after pairing.

AI marketplace (platform.twentyfirst.ai)

  1. Deploy apps/platform.twentyfirst.ai on Twentyfirst infrastructure.
  2. Run Prisma migrations: npm run db:platform:migrate.
  3. Configure Stripe, Replicate, OpenAI, and email (see app .env.example).
  4. Run the email scam checker worker separately if needed: npm run email:worker -w @repo/platform.twentyfirst.ai.

Corporate media (twentyfirst.media)

  1. Deploy apps/twentyfirst.media on Twentyfirst infrastructure.
  2. Push schema: npm run db:media:push (or migrate in production).
  3. Configure S3/SES, Stripe, and OAuth as needed (see app .env.example).
  4. Restrict /admin to trusted networks or SSO.

AI academy (twentyfirst.academy)

  1. Deploy apps/twentyfirst.academy on Twentyfirst infrastructure.
  2. Push schema: npm run db:academy:push (or migrate in production).
  3. Configure NEXTAUTH_*, Stripe (STRIPE_MODE, test/live keys, webhook secret), and optional S3 for lesson assets (see app .env.example).
  4. Register Stripe webhook endpoint for checkout.session.completed and related subscription events.
  5. Restrict /admin to trusted networks or SSO.

Verification

npm run smoke -w @repo/chat.twentyfirst.ai
npm run smoke -w @repo/twentyfirst.ai

Rollback

Redeploy the previous image/tag per app. If a migration is incompatible, restore the database from a pre-migrate snapshot before rolling back.