Skip to content

Operate a self-hosted Chorus

This guide covers operating a self-hosted Chorus after it is deployed: getting the first person signed in, configuring authentication, upgrading safely, and the cookie and reverse-proxy settings that matter behind HTTPS. For how to stand a deployment up, see Deploy in production or Deploy with Docker.

A fresh deployment can use managed local accounts without an identity provider. Configure the Super Admin credentials below, then create a local-account workspace and invite its first Workspace Admin. This differs from the optional Default Auth demo/bootstrap path. To enable that legacy path, set both variables:

Terminal window
DEFAULT_USER='admin@example.com' \
DEFAULT_PASSWORD='choose-a-strong-password'

When both variables are set, the configured email can use Default Auth. On first successful sign-in, Chorus creates the user and workspace as needed, but not a managed local account or local Workspace Admin role. Sign-in starts with an email or username; choose the intended role or workspace if several match.

After verifying managed local-account or OIDC access and Super Admin sign-in, remove DEFAULT_USER and DEFAULT_PASSWORD from the application environment and recreate or restart it. This disables Default Auth, not managed local-account password login, and does not itself revoke already-issued sessions.

For the installer-managed Compose stack, keep both values nonempty in the private env file: the base Compose file requires them during interpolation. Instead, save this container-environment override as oidc-only.yml in the private directory:

services:
app:
environment:
DEFAULT_USER: ""
DEFAULT_PASSWORD: ""

Add -f "$HOME/.config/chorus-self-host/$PROJECT/oidc-only.yml" to your complete dc command, before "$@", then run dc config --quiet and dc up -d --wait --force-recreate app. Preserve this override in every later start or upgrade. The saved local wrapper does not load oidc-only.yml; do not use it unchanged after this step. Prepare the desired app image and use the full Compose invocation instead. The tunnel recipe in the local guide already includes this file when present.

Default Auth and Super Admin credentials come from the deployment environment. Managed local accounts and OIDC are configured per workspace in the database, not with a new local-auth environment switch.

The /admin Super Admin panel is bootstrapped from environment variables, not from a database account:

VariableDescription
SUPER_ADMIN_EMAILEmail for the Super Admin account that can reach the /admin panel.
SUPER_ADMIN_PASSWORD_HASHBcrypt hash of the Super Admin password (never the plaintext).

Generate the hash and pass it as SUPER_ADMIN_PASSWORD_HASH:

Terminal window
node -e "console.log(require('bcryptjs').hashSync('your-password', 10))"

Sign in as the Super Admin to create a local-account workspace and invite its first Workspace Admin. Share the one-time link privately; Chorus does not send invitation or password-reset email. The recipient sets a password, then signs in separately. Invitations and reset links expire after 24 hours; generating a replacement invalidates the older link. An active local Workspace Admin manages further users and resets. This role is not the deployment Super Admin and is not granted by DEFAULT_USER.

Local accounts and OIDC are mutually exclusive within a workspace. Choose the appropriate type when creating it; existing workspaces cannot be converted by editing their authentication mode. Back up the database and persistent NEXTAUTH_SECRET: local passwords are bcrypt hashes in the database (work factor 12), while local sessions use that signing secret. Passwords must have at least 8 characters and at most 72 UTF-8 bytes. Use HTTPS and COOKIE_SECURE=true for shared access. See Authentication setup for account procedures.

The historical override filename oidc-only.yml above only disables Default Auth; it also works for managed local-account workspaces and does not enable OIDC.

Single sign-on is not configured through environment variables. The Super Admin configures OIDC per workspace from the /admin panel, and the settings are stored in the database. Each workspace has its own issuer URL and client ID; SSO is on for a workspace once both are set. Chorus uses PKCE and does not require a client secret. The deployment-level bootstrap gets the Super Admin in, and the Super Admin then sets up SSO for each workspace from the admin panel, with no separate secret provisioning at the deployment layer.

Chorus applies pending database migrations automatically at startup (prisma migrate deploy). You do not run migrations by hand — starting a new version applies whatever is outstanding before the web application begins serving.

To upgrade:

  1. Back up your database first. For an external PostgreSQL, take a backup; for an embedded or volume-backed database, back up the data volume.
  2. Pull the new version — npm update -g @chorus-aidlc/chorus for the npm package, or pull the new container image for a Docker deployment.
  3. Restart Chorus. Migrations apply automatically on boot, then the app starts.

For local-mode installations whose required overrides the wrapper loads, run bash ./default_deploy_local.sh (update CHORUS_IMAGE first for pull mode). For tunnel mode or oidc-only.yml/other custom overrides, prepare the desired app image and use your complete Compose invocation instead. Never rerun the installer to upgrade. PostgreSQL major upgrades remain a separate offline migration. See Self-host on a local machine.

0.19.1 includes a migration that only adds a nullable column, so existing data is untouched. Upgrade the Chorus server before upgrading the chorus CLI that runs your agent daemons. Older daemons keep working against the new server, receiving Idea creation and Research requests in the form they already understand. For an emergency rollback of the new request format, set CHORUS_DEDICATED_OPERATION_WRITES=false and restart the server: new requests use the older format, and ones already queued are still delivered. Roll back the CLI separately if you need to. Do not roll the server back to a version older than 0.19.1 while requests created by 0.19.1 are still pending or running, and do not remove the new column.

Back up the signing secret together with your database and keep it stable. To retain an auto-generated secret, Docker needs a persistent /app/data volume even with external PostgreSQL; the npm launcher uses ~/.chorus-data/.secret by default. If you previously used a public placeholder, the replacement secret requires users of the built-in email/password login and the Super Admin to sign in again. A secure secret you set explicitly is kept as is. For CDK redeployments, supply the same nextAuthSecret: omitting it generates a new value at each synth. API keys and OIDC tokens are unaffected by this rotation. See Docker migration guidance.

Two settings matter when Chorus runs behind TLS termination or a reverse proxy:

VariableSet toWhen
COOKIE_SECUREtrueThe site is served over HTTPS (including behind a proxy that terminates TLS). Secure cookies are only sent over encrypted connections.
COOKIE_SECUREfalseOnly for trusted, internal HTTP-only networks. Cookies are then sent over unencrypted HTTP, so never use this on the public internet.
NEXTAUTH_URLThe public base URLChorus runs behind a reverse proxy. Set it to the externally visible address (for example https://chorus.example.com) so redirects and links resolve correctly.

Redis serves two separate purposes: Pub/Sub propagates SSE live updates, while a separate request connection stores shared authentication counters. All replicas must use the same Redis for consistent limits; working SSE delivery alone does not prove those counters are healthy.

  • Identity lookup allows 30 attempts per 60 seconds; local password login and invitation/reset acceptance each allow 10 per 60 seconds. These are fixed limits, not environment options.
  • Without a trusted client address, limits apply to the normalized identifier, workspace plus identifier for local login, or credential token for acceptance, not one global bucket. With a trusted address, both an address-plus-identity bucket and a separate address bucket apply per action, preventing identifier rotation from bypassing the address limit.
  • Rejection returns HTTP 429, RATE_LIMITED, and Retry-After in seconds (rounded up, minimum 1). This does not cover every login/API endpoint; keep edge limits for the full authentication surface.

Configuration and defaults:

  • A nonempty REDIS_URL takes precedence. Otherwise REDIS_HOST enables TLS (rediss), with REDIS_PORT=6379, REDIS_USERNAME=default, and optional REDIS_PASSWORD. The username is used when a password is supplied. For non-TLS Redis use an explicit redis:// URL. With neither URL nor host set, Redis is unconfigured.
  • AUTH_TRUST_PROXY is off unless exactly true. AUTH_TRUST_PROXY_HOPS defaults to 1 and must be an integer from 1 through 16. Addresses are counted from the right of X-Forwarded-For; missing, too-short, or invalid chains disable address limiting, not identity limiting. Enable trust only behind an enforced append/overwrite proxy chain with no direct or intermediate bypass. HTTPS alone does not establish that trust.
  • Bundled CDK ALB mode sets trust to true, hops 1. CloudFront mode sets trust to false, hops 2: VPC traffic can bypass CloudFront at the private origin. Do not enable two-hop trust until that bypass is closed.

No Redis configuration means process-local counters and a warning. When configured Redis is connecting, unavailable, or rejects commands (including READONLY/OOM), the limiter uses per-process in-memory insurance counters, not shared counters. Commands time out after 500 ms; a socket with pending commands and no reply is recycled after 2,000 ms. Unfulfilled commands are not replayed on reconnect. The 500 ms bound is per command, not a whole-request response-time guarantee.

Watch auth-throttle logs for degradation and restoration. Insurance warnings are limited to once per 60 seconds, with a 5-second first-connection grace period. Memory counters share a 10,000-key capacity with least-recently-used eviction: they are best-effort, reset on restart, and are not merged back into Redis on recovery. An unexpected limiter backend error outside normal insurance handling is logged and permits that check (fail-open). A Redis outage does not deliberately block all sign-ins.

Account validation is fail-closed and remains separate: disabled/deleted local users, an incompatible workspace mode, or a stale session version cannot authorize a local session. Failure to read account state does not grant access. Insurance also cannot restore cross-instance SSE; local events may continue while cross-instance updates are unavailable. Restore Redis and check both authentication counters and live updates before declaring recovery.