Self-hosting and setup

Everything needed to run NextForge: the services, their settings, and how to run, migrate and deploy them.

The pieces

AppWhatPort
apps/apiNestJS API, the system of record. REST plus Socket.IO for live boards, chat and doc co-editing.3010
apps/webNext.js app, this site and this guide.3000
apps/adminSuperadmin panel (Vite): organizations, platform settings, mobile app uploads.5174
apps/ai-backendZenit, the AI service (FastAPI on Groq's free tier). Only the API talks to it.8000
apps/scannerGo worker that runs Trivy (dependencies) and OWASP ZAP (live app) scans.8090
apps/mobileFlutter Android app, released as an APK.n/a

Also needed: PostgreSQL and Redis. Optional: an S3 bucket (uploads), OneSignal (email and push), a GitHub App, Google OAuth (calendar), a Slack webhook.

Run it locally

Needs Node 20+, pnpm (via corepack), Python 3.12 for Zenit, and Go only if you run the scanner.

corepack enable
pnpm install
pip install -r apps/ai-backend/requirements.txt

# database tables
pnpm --filter nextforge-api run migration:run

# the first admin: prints a one-time password
pnpm --filter nextforge-api run seed:admin -- you@company.com "Your Name" "Your org"

# api, web, admin, Zenit and the scanner together
pnpm run dev

The seeded admin is platform staff and owns a first workspace. Staff must turn on two-step sign-in (an authenticator app) under Account before the admin panel opens.

Before committing, run pnpm run check: typecheck, tests, lint, and a rule that no API file goes over 400 lines.

Settings

Each app reads a .env file next to it. These files are gitignored; never commit them. Settings marked * are required: the API refuses to start without them rather than run with a weak default. Make random keys with openssl rand -hex 32.

API: core

SettingWhat it is
DATABASE_URL *Postgres connection string (or DATABASE_HOST, _PORT, _NAME, _USER, _PASSWORD, _SSL)
REDIS_HOSTRequired in production (sessions and rate limits are shared through it). Plus REDIS_PORT, REDIS_PASSWORD, REDIS_TLS
JWT_SECRET *Signs access tokens. At least 32 characters.
DATA_ENCRYPTION_KEY *Encrypts personal data at rest. 64 hex characters.
BLIND_INDEX_KEY *Lets encrypted emails be looked up. 64 hex characters, different from the one above.
WEB_URLWhere the web app lives; used in emails and redirects
STUDIO_ORIGINSComma-separated origins allowed by CORS (the web and admin URLs)
PUBLIC_SIGNUPtrue lets anyone create a workspace
PORTDefaults to 3010

API: services

SettingWhat it is
ZENIT_URLZenit's address, default http://127.0.0.1:8000
ZENIT_API_KEYShared secret with Zenit; same value on both sides
SCANNER_SERVICE_URLThe scanner's address
AWS_REGION, AWS_BUCKET_NAME, AWS_ACCESS_KEY, AWS_SECRETS3 for uploads. Everything goes under STORAGE_PREFIX (default nextforge/). Without these, uploads answer 503.
ONESIGNAL_APP_ID, ONESIGNAL_API_KEYEmail and phone push
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URIGoogle Calendar connection
SLACK_WEBHOOK_URLFailed-build alerts to Slack
GITHUB_APP_*, GITHUB_WEBHOOK_SECRETSee GitHub App below

Other apps

SettingWhat it is
NEXT_PUBLIC_API_URL *Web: the API's public URL (also used for the live socket)
VITE_API_URL *Admin: the API's public URL
GROQ_API_KEY *Zenit: a free key from console.groq.com
ZENIT_API_KEY *Zenit: the same shared secret as on the API

Zenit stays on Groq's free tier on purpose. Long jobs, like turning a PRD into a backlog, are split into many small calls that fit its per-minute limit, and pause (resumable) if the daily limit runs out.

GitHub App

Create it once; every workspace connects to it from Settings → Integrations.

  1. GitHub → Settings → Developer settings → GitHub Apps → New GitHub App (under your organization if the repos are there).
  2. Callback URL https://API/integrations/github/callback, webhook URL https://API/integrations/github/webhook, a long random webhook secret. Turn on Request user authorization (OAuth) during installation: NextForge uses it to check who is installing.
  3. Repository permissions: Issues read and write; Contents, Pull requests, Actions read; Metadata read. Events: Issues, Issue comment, Push, Pull request, Create, Workflow run.
  4. Generate a client secret and a private key (.pem).
GITHUB_APP_ID=123456
GITHUB_APP_SLUG=nextforge
GITHUB_APP_CLIENT_ID=Iv23li...
GITHUB_APP_CLIENT_SECRET=...
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n"
GITHUB_WEBHOOK_SECRET=...

Restart the API and Settings → Integrations shows Connect GitHub to owners.

Deploy

Pushing to main or develop runs the publish workflow: it builds the API, web and scanner images, pushes them to the GitHub Container Registry, and deploys over SSH. A separate workflow scans the images with Trivy (vulnerabilities, SBOM, secrets).

To try the image on one machine first: put the API settings in docker/app.env and run docker compose up -d --build (it brings Redis; point the database settings at your Postgres).

Database migrations run by themselves: in production the API applies any pending ones when it starts, before it takes traffic, each in a transaction. If one fails, the API doesn't start and the error is in its log. Locally, run migration:run yourself.

Good to know

  • Every request records which app sent it (web, admin, or the phone app with its version) in the audit log, the sessions list and the server's request log.
  • Live doc co-editing keeps each open page in the API's memory. That's fine on one API instance; running several needs a shared store and the Socket.IO Redis adapter first.