Running a binary
Prerequisites
- Go 1.26.1 — every module declares this exact version
- PostgreSQL 14+, reachable at
DATABASE_URL - Node 20+, for each app's bundled
fe/ pgvectorfor knowledge-base search. Optional fornextneural_assistandnextneural_converse(the migration tolerates it being missing and search is simply unavailable); required fornextneural_coach, whose migration has no fallback.nextneural_intelonly: a C compiler onPATH(CGO_ENABLED=1, for the VAD binding) andffmpegonPATH(FFMPEG_BINARYif it lives elsewhere)
Start a binary
cd apps/nn_converse
cp .env.example .env # fill in DATABASE_URL at minimum
make -C ../.. build-migrations APP=nn_converse
make -C ../.. run APP=nn_converse
Or from the repo root:
make run APP=nn_converse
make run defaults to ./cmd/server, which nextneural_coach does not have — its
server lives in cmd/nn_coach. Run it with make run APP=nn_coach BIN=nn_coach,
or use its own apps/nn_coach/dev.sh, which also starts the front end.
Migrations
Migrations apply themselves on every boot. Every main.go calls
db.Migrate(cfg.DatabaseURL) before serving, so pointing DATABASE_URL at an
empty database creates the whole schema on first start. Re-running is safe —
only pending migrations are applied.
make build-migrations is a compile-time step, not a database one.
Migrations live twice: the canonical .sql files in db/migrations/, and a
copy under internal/db/migrations/ that go:embed bakes into the binary
(Go's embed directive cannot reach outside its own package directory). make build and make run do this copy for you; running go run directly does not,
which is what produces:
pattern migrations/*.sql: no matching files found
Recovering from a failed migration
If a migration fails partway, golang-migrate marks the schema version dirty
and refuses every later boot:
migrate: migrate up: migration failed: ... (details: pq: relation "x" already exists)
Inspect and clear it by hand:
SELECT * FROM schema_migrations; -- version, dirty
UPDATE schema_migrations SET version = 1, dirty = false;
Fix whatever the migration collided with before restarting, or the next boot fails the same way.
Configuration
Read from the environment, with .env as a fallback — real environment
variables always win over the file. Point ENV_FILE at another path to use a
different one.
Required everywhere
| Variable | Notes |
|---|---|
DATABASE_URL | postgres://user:pass@host:5432/dbname?sslmode=disable |
JWT_SECRET | Signing secret for session tokens |
Common across binaries
| Variable | Default | Purpose |
|---|---|---|
PORT | 8080 | HTTP listen port (nextneural_coach uses NN_COACH_ADDR, default :8010) |
APP_BASE_URL | — | Where the front end is served; used in emailed links |
PUBLIC_BASE_URL | — | Publicly reachable base for telephony callbacks |
CORS_ALLOWED_ORIGINS | — | Comma-separated origins for the browser console |
ALLOWED_EMAIL_DOMAINS | — | Domains that skip the waitlist gate |
SUPERADMIN_EMAIL_DOMAINS | — | Domains whose accounts are platform superadmins |
SUPERADMIN_EMAIL / SUPERADMIN_PASSWORD | — | Bootstrap superadmin account |
RESEND_API_KEY / RESEND_SENDER_EMAIL | — | Email delivery for invites, OTPs, resets |
FIREBASE_ENABLED, FIREBASE_PROJECT_ID, … | — | Enables POST /api/auth/firebase |
UPLOAD_DIR | ./data/uploads | Local file storage |
Without an email provider configured, signup OTPs and invites cannot be delivered — the failure is reported rather than silently swallowed.
nextneural_converse
| Variable | Purpose |
|---|---|
LLM_PROVIDER, LLM_MODEL, LLM_API_KEY, LLM_BASE_URL | Conversation model |
STT_PROVIDER, DEEPGRAM_API_KEY, SARVAM_API_KEY | Speech recognition |
TTS_PROVIDER, NEXTNEURAL_TTS_URL, NEXTNEURAL_TTS_TOKEN | Speech synthesis and voice cloning |
AWS_*, BEDROCK_* | Embeddings for knowledge-base search |
VOICE_VAD_*, VOICE_ROUTER_*, VOICE_LANG_LOCK_WORDS | Turn-taking and language tuning |
PUBLIC_BASE_URL | Required to run a campaign — telephony must reach the webhooks |
nextneural_assist
| Variable | Purpose |
|---|---|
SARVAM_API_KEY, SARVAM_STT_MODEL | Live transcription |
AWS_*, BEDROCK_* | Suggestion model and embeddings |
SETTINGS_ENCRYPTION_KEY | Encrypts stored telephony credentials |
AGENT_ASSIST_LIVE_TRANSCRIPTION | Toggles live transcription |
EXOTEL_SAMPLE_RATE | Must match the rate in the Exotel stream URL |
nextneural_intel
Beyond the common set, nextneural_intel needs ffmpeg and a C toolchain at build
time. Model selection is configured in-app through
model routing rather than by environment
variable.
nextneural_coach
| Variable | Default | Purpose |
|---|---|---|
NN_COACH_ADDR | :8010 | Listen address |
NN_COACH_DATABASE_URL | falls back to DATABASE_URL | Database |
OPENROUTER_API_KEY, OPENROUTER_BASE_URL | — | Model access |
NN_COACH_AGENT_MODEL, _PERSONA_MODEL, _JUDGE_MODEL, _BRIEF_MODEL | — | Per-stage model choice |
SARVAM_API_KEY, NN_COACH_SARVAM_VOICE, NN_COACH_TTS_PROVIDER | — | Voice practice |
JUDGE_API_BASE_URL, JUDGE_API_TOKEN | — | Delegates scoring to nextneural_intel |
VOICE_API_BASE_URL, VOICE_API_TOKEN | — | Automated voice runs against a deployed flow |
NN_COACH_DB_AUTO_CREATE | true | Creates its own database at boot if missing |
NN_COACH_DB_AUTO_CREATE needs the database role to hold CREATEDB. Without
it, create the database yourself:
sudo -u postgres psql -c "CREATE DATABASE nn_coach OWNER st_user;"
Health checks
| Binary | Endpoint |
|---|---|
nextneural_converse, nextneural_intel, nextneural_assist | GET /api/healthz |
nextneural_coach | GET /health |
None require a credential. nextneural_intel also reports its build identity, so a
deployed machine can be asked what it is actually running:
{ "status": "ok", "version": "1.4.2", "commit": "0f9c19a", "built": "2026-09-18T10:22:31Z" }
Ports
All four default to 8080 except nextneural_coach (8010), so a host running more
than one needs PORT set explicitly per service.
Building for deployment
make build APP=nn_converse # local build
make release-linux APP=nn_converse # static linux/amd64 binary
make build-all # every app
Three of the four cross-compile to a fully static binary with no runtime libc
dependency. nextneural_intel is the exception — its VAD binding needs cgo.