Skip to Content
Polyant is open source under AGPL-3.0 — star us on GitHub.
OperationsDeployment

Deployment

Polyant is designed to run on any container platform. This document gives concrete recipes for the most common targets.

Architecture recap

You need to deploy three components:

ComponentDefault portImage
Engine (NestJS)4000built from packages/engine/Dockerfile
Web (Next.js)3000built from packages/web/Dockerfile
PostgreSQL 16 + pgvector5432pgvector/pgvector:pg16 (managed DB preferred in production)

Both Dockerfiles expect the build context to be the monorepo root, not the package directory:

docker build -f packages/engine/Dockerfile -t polyant-engine . docker build -f packages/web/Dockerfile -t polyant-web .

Required environment variables

Only the production-critical vars are listed here. For the complete inventory (defaults, tunables, debug flags), see Environment Variables Reference.

Engine

VariableRequiredNotes
DATABASE_URLyesFull PostgreSQL connection string with pgvector extension
ENCRYPTION_KEYyes32-byte hex key (openssl rand -hex 32). Lost key = unrecoverable instance secrets
AUTH_SECRETyes32+ char random string. Must match the web package
AUTH_INTERNAL_SECRETyes (for credentials login)Shared secret used to authenticate the web → engine credentials bridge. Must match the same env var on the web package.
CORS_ORIGINSyes in prodComma-separated allowlist of web origins (e.g. https://app.example.com). When unset under NODE_ENV=production, the engine rejects all cross-origin requests. A credentialed wildcard (*) is refused at startup — list explicit origins.
TRUST_PROXYyes behind a proxyNumber of trusted reverse-proxy hops (e.g. 1 behind Render/Railway/nginx, 2 behind two layers). Defaults to 0 (trust nothing). When the engine sits behind a proxy and this is left at 0, the Twilio webhook signature check breaks because X-Forwarded-Host/-Proto are ignored.
BASE_URLrecommendedPublic base URL of the engine — used to build webhook callback URLs.
INITIAL_ADMIN_EMAILoptionalFirst-boot Superadmin email. Defaults to administrator@local.
INITIAL_ADMIN_PASSWORDoptionalFirst-boot Superadmin password. If unset, a random password is generated and printed once to the engine logs at first boot.
PLATFORM_S3_BUCKET, PLATFORM_S3_REGION, PLATFORM_S3_ACCESS_KEY_ID, PLATFORM_S3_SECRET_ACCESS_KEYoptionalEnables attachment persistence (WhatsApp / Telegram media). Without these, attachments are not durable across restarts.
NODE_ENVrecommendedSet to production. Activates the strict CORS default above and disables verbose error responses.

Tunables — review before going live

These have sensible defaults but you almost certainly want to revisit them when traffic shape becomes clear. See env-vars.md for each variable’s contract.

VariableDefaultWhy you might tune it
MESSAGE_SOFT_DEBOUNCE_MS2000Inbound message coordinator — coalescence window for WhatsApp/Telegram burst fragments before the pipeline fires. Lower it on a chatty bot, raise it for users who think in long bullets.
MESSAGE_TYPING_DELAY_MS1500Delay before the typing indicator is shown. Set to a value ≤ the soft-debounce so the user gets feedback during the wait.
MESSAGE_MAX_RESTARTS3Cap on consecutive cancel-and-restart cycles when fragments keep arriving mid-pipeline.
AGENT_CALL_TIMEOUT_MS60000Max wall-clock duration of a single sub-agent invocation (virtual agent channel).
SSE_MAX_CONNECTIONS / SSE_MAX_CONNECTIONS_PER_USER50 / 5Activity-stream concurrency caps. Bump if you have many admin panel tabs open in parallel.
ANALYTICS_RETENTION_DAYS90Daily cleanup of ai_logs and pipeline_traces. Lower to save storage, raise for longer dashboard windows.
KNOWLEDGE_MAX_DOCS_PER_INSTANCE500Hard cap on knowledge documents per instance. Uploads beyond this return HTTP 400.
DEDUP_SIMILARITY_THRESHOLD0.90Cosine threshold for memory de-duplication (extraction layer).
DATETIME_TIMEZONE / DATETIME_LOCALEUTC / en-USDefault timezone and locale for the supervisor’s {{datetime}} template substitution.

Web

VariableRequiredNotes
AUTH_SECRETyesMust match engine
AUTH_INTERNAL_SECRETyes (for credentials login)Must match engine. Without it the Credentials provider is disabled and email/password sign-in stops working.
AUTH_TRUST_HOSTyes behind a proxySet to true behind a reverse proxy. Without it Auth.js rejects the request host as untrusted and login fails.
DATABASE_URLyesAuth.js adapter writes sessions here (same database as the engine)
NEXT_PUBLIC_API_URLyesPublic URL of the engine (browser-side; embedded at build time)
INTERNAL_ENGINE_URLrecommendedServer-side URL the web’s Credentials provider uses to call POST /api/auth/credentials/verify on the engine. In production set this to the engine’s internal/cluster address — never via the public proxy. Defaults to http://localhost:4000 (development only).
GOOGLE_CLIENT_IDoptionalOAuth credentials. Without both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET the Google sign-in button is hidden and the provider is disabled.
GOOGLE_CLIENT_SECREToptionalSee above.
AUTH_ALLOWED_DOMAINSoptionalRestrict Google sign-in to specific email domains (comma-separated).

Behind a reverse proxy — checklist

Run-of-the-mill production setups put both services behind nginx / Caddy / a managed platform’s TLS terminator. Two settings are easy to forget:

  1. Web: AUTH_TRUST_HOST=true — Auth.js otherwise refuses requests whose host doesn’t match a locked-down list.
  2. Engine: TRUST_PROXY=<hops> — the default 0 means X-Forwarded-Host / X-Forwarded-Proto are ignored. The Twilio webhook signature is computed against the request URL, so if those headers are dropped the HMAC check fails and inbound WhatsApp/SMS stops working with a 403.

If neither service is behind a proxy (single-host deployment with the engine directly exposed), leave both at their defaults.

Docker Compose (single host)

For small deployments, Docker Compose on a single VM works well.

services: postgres: image: pgvector/pgvector:pg16 restart: unless-stopped environment: POSTGRES_DB: polyant POSTGRES_USER: polyant POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data engine: build: context: . dockerfile: packages/engine/Dockerfile restart: unless-stopped depends_on: [postgres] environment: NODE_ENV: production DATABASE_URL: postgresql://polyant:${POSTGRES_PASSWORD}@postgres:5432/polyant ENCRYPTION_KEY: ${ENCRYPTION_KEY} AUTH_SECRET: ${AUTH_SECRET} AUTH_INTERNAL_SECRET: ${AUTH_INTERNAL_SECRET} CORS_ORIGINS: https://your-web.example.com TRUST_PROXY: "1" BASE_URL: https://your-engine.example.com ports: ["4000:4000"] web: build: context: . dockerfile: packages/web/Dockerfile args: NEXT_PUBLIC_API_URL: https://your-engine.example.com AUTH_SECRET: ${AUTH_SECRET} restart: unless-stopped depends_on: [engine] environment: NODE_ENV: production DATABASE_URL: postgresql://polyant:${POSTGRES_PASSWORD}@postgres:5432/polyant AUTH_SECRET: ${AUTH_SECRET} AUTH_INTERNAL_SECRET: ${AUTH_INTERNAL_SECRET} AUTH_TRUST_HOST: "true" INTERNAL_ENGINE_URL: http://engine:4000 GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} ports: ["3000:3000"] volumes: pgdata:

Put an HTTPS reverse proxy (Caddy, nginx, Traefik) in front.

Render

Polyant works well on Render  with two web services plus a managed PostgreSQL.

  1. PostgreSQL: create a Render PostgreSQL instance with pgvector enabled (via the CREATE EXTENSION query in the shell).
  2. Engine service:
    • Environment: Docker
    • Dockerfile path: packages/engine/Dockerfile
    • Docker build context: . (repo root)
    • Health check: /health
    • Plan: Starter+ (memory-intensive due to tools)
  3. Web service:
    • Environment: Docker
    • Dockerfile path: packages/web/Dockerfile
    • Docker build context: .
    • Build args: NEXT_PUBLIC_API_URL, AUTH_SECRET
  4. Add the environment variables listed above to each service.
  5. Migrations run automatically at engine startup (packages/engine/scripts/docker-start.sh invokes db:migrate before the server).

Fly.io

fly launch --no-deploy # in repo root

Edit the generated fly.toml:

  • primary_region close to your users
  • Set secrets: fly secrets set ENCRYPTION_KEY=... AUTH_SECRET=...
  • Use Fly PostgreSQL: fly pg create --name polyant-db and attach it.

Deploy the engine and web as two separate Fly apps (recommended), sharing the same PostgreSQL cluster.

Kubernetes

The Dockerfiles are stateless runners — any Kubernetes platform works. Key points:

  • Persistent volume: not required for engine or web. Only PostgreSQL needs persistence.
  • Ingress: terminate HTTPS here and set AUTH_TRUST_HOST=true on the web deployment.
  • Secrets: use Kubernetes Secrets or an external secret manager for ENCRYPTION_KEY and AUTH_SECRET.
  • Migrations: run as a Kubernetes Job (node dist/database/migrate.js) before rolling out a new engine version, or let the engine run them at startup.
  • Scaling: the engine is horizontally scalable for HTTP traffic. However, channel adapters (Telegram long-poll, Slack Socket Mode) are stateful — run exactly one engine replica with channels enabled, or add distributed coordination.

Migration strategy

Polyant uses Drizzle migrations stored in packages/engine/src/database/migrations/.

  • Migrations are SQL files applied in order.
  • They are executed automatically at engine startup via packages/engine/scripts/docker-start.sh.
  • To run manually: npm run db:migrate.
  • Rolling back: create a new forward-migration that undoes the change. Drizzle migrations are forward-only.

Observability

  • Health check: GET /health returns {status: "ok"}.
  • Pipeline tracing: pipeline_traces table — per-request phase timings (context prep, tool building, LLM call, total).
  • AI call logs: ai_logs table — per-LLM-call token counts and USD cost.
  • LangSmith: enable per-instance in the Settings tab for distributed tracing.
  • Application logs: structured JSON on stdout — ship to your log aggregator.

Backups

Back up the PostgreSQL database. It contains:

  • All conversations and messages
  • All memories (embeddings in pgvector)
  • All encrypted instance secrets and channel configs (decryptable only with your ENCRYPTION_KEY)
  • All skills, prompts, tools

Back up your ENCRYPTION_KEY separately. Losing it means the encrypted data in the DB becomes unreadable.

Last updated on