Environment variables

The full list of environment variables for running a self-hosted Concierto server.

A self-hosted Concierto server reads its configuration from environment variables at startup. Database, sign-in, email, storage, signup allowlists all live here. This page groups every variable by purpose: each section spells out what happens if you leave it unset and which ones you must set in production. For how to actually configure the auth-related ones, see Sign-in and signup configuration.

Core server variables

These are the core variables you must think about before deploying. Some have defaults that let the server start, but in production you should set the required ones explicitly.

VariableDefaultRequired in production?
DATABASE_URLpostgres://concierto:concierto@localhost:5432/concierto?sslmode=disableYes
PORT8080No (unless you change the port)
JWT_SECRETchange-me-in-productionYes (the default is unsafe)
APP_ENVemptyYes (must be production)
FRONTEND_ORIGINemptyYes (self-host must set its own domain)
CONCIERTO_DEV_VERIFICATION_CODEemptyNo (must stay empty in production)

Keep CONCIERTO_DEV_VERIFICATION_CODE empty in production. A fixed local test code is disabled by default, but if you opt in with CONCIERTO_DEV_VERIFICATION_CODE=888888, anyone who can request a code can sign in with that fixed value while APP_ENV is non-production. The shortcut is ignored when APP_ENV=production.

Database connection pool

VariableDefaultDescription
DATABASE_MAX_CONNS25pgxpool max connections. The daemon polls frequently (every 3s) and uses connections; larger deployments may need a higher value
DATABASE_MIN_CONNS5Minimum idle connections

When unset, the values above are used. not pgx's built-in 4/NumCPU defaults, which previously caused pool exhaustion in production.

Email configuration

Concierto supports two delivery backends. Resend for cloud deployments, or an SMTP relay for internal / on-premise networks. SMTP_HOST takes priority over RESEND_API_KEY when both are set.

Resend

VariableDefaultDescription
RESEND_API_KEYemptyResend API key
RESEND_FROM_EMAILnoreply@nstack.localSender address (must be a domain verified in your Resend account; also reused as the From: header when SMTP is in use)

SMTP relay

VariableDefaultDescription
SMTP_HOSTemptySMTP relay hostname. Setting this activates SMTP mode and overrides Resend
SMTP_PORT25SMTP port. Use 587 for STARTTLS submission; port 465 (SMTPS / implicit TLS) is not supported
SMTP_USERNAMEemptySMTP username. Leave empty for unauthenticated relay
SMTP_PASSWORDemptySMTP password
SMTP_TLS_INSECUREfalseSet true to skip TLS certificate verification (private CA / self-signed only)

STARTTLS is upgraded automatically when the server advertises it. The dial timeout is 10s and the whole SMTP session has a 30s deadline, so a black-holed relay can't hang the auth handler.

Behavior when neither is set: the server does not error, but every email that should have been sent (verification codes, invite links) is written to the server's stdout only. Convenient for local development. Copy the code out of the server logs; in production, forgetting to set this creates a silent black hole, with users never receiving email and no error surfaced.

Google OAuth configuration

Optional. Leave unset for email + verification code only; configure it to add "Sign in with Google" on the sign-in page.

VariableDefaultDescription
GOOGLE_CLIENT_IDemptyGoogle Cloud OAuth client ID
GOOGLE_CLIENT_SECRETemptyGoogle Cloud OAuth secret
GOOGLE_REDIRECT_URIhttp://localhost:3000/auth/callbackOAuth callback URL (self-host: replace with your frontend domain)

Takes effect at runtime: the frontend reads these settings via /api/config at runtime, so changing them requires no frontend rebuild or redeploy, restart the server and they apply.

Full setup (including Google Cloud Console steps) is in Sign-in and signup configuration.

File storage configuration

Concierto stores user-uploaded attachments (images and files in comments). S3 is preferred; if S3 is not configured, it falls back to local disk.

S3 / S3-compatible storage

VariableDefaultDescription
S3_BUCKETemptyBucket name only (for example my-bucket). Do not include the .s3.<region>.amazonaws.com suffix, the server constructs the public host from S3_BUCKET + S3_REGION. Setting this enables S3 storage
S3_REGIONus-west-2AWS region. Must match the bucket's actual region, it is used both for SDK signing and for building the public URL
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYemptyStatic credentials. When both are unset, the AWS SDK default credential chain is used (IAM role / environment credentials)
AWS_ENDPOINT_URLemptyCustom S3-compatible endpoint (for example MinIO). Setting this switches to path-style URLs

When S3_BUCKET is unset: the server logs "S3_BUCKET not set, cloud upload disabled" at startup, and all uploads fall back to local disk.

Public URLs are constructed in this order of priority:

  1. https://<CLOUDFRONT_DOMAIN>/<key> if CLOUDFRONT_DOMAIN is set.
  2. <AWS_ENDPOINT_URL>/<S3_BUCKET>/<key> (path-style) if AWS_ENDPOINT_URL is set.
  3. https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key> (virtual-hosted-style). When S3_BUCKET contains dots, the server falls back to https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key> (path-style) because the AWS-issued wildcard TLS certificate does not validate dotted bucket hosts.

Local disk (when S3 is not configured)

VariableDefaultDescription
LOCAL_UPLOAD_DIR./data/uploadsLocal storage directory
LOCAL_UPLOAD_BASE_URLempty (returns relative paths)Public base URL, leave unset and the frontend can't resolve a full URL for attachments

CloudFront (optional)

If you front S3 with CloudFront, three variables apply: CLOUDFRONT_DOMAIN, CLOUDFRONT_KEY_PAIR_ID, CLOUDFRONT_PRIVATE_KEY (or CLOUDFRONT_PRIVATE_KEY_SECRET to read from Secrets Manager). Skip them if you don't use CloudFront, they don't conflict with S3 configuration.

VariableDefaultDescription
COOKIE_DOMAINemptyScope of the session cookie
  • Empty: the cookie is valid only on the exact host visited (correct for single-host deployments)
  • Set to .example.com: the cookie is shared across subdomains (so app.example.com and api.example.com share a sign-in session)
  • Warning: it cannot be an IP address (browsers ignore it)

Restricting who can sign up

Three allowlist layers combine by priority. If any layer is set to a non-empty value, emails that don't match are rejected, even ALLOW_SIGNUP=true won't override that.

VariableDefaultDescription
ALLOWED_EMAILSemptyExplicit email allowlist (comma-separated). When non-empty, only listed emails can sign up
ALLOWED_EMAIL_DOMAINSemptyDomain allowlist (comma-separated). When non-empty, only listed domains can sign up
ALLOW_SIGNUPtrueSignup master switch. Set false to disable signup entirely

The counterintuitive part: ALLOWED_EMAIL_DOMAINS=company.io + ALLOW_SIGNUP=true does not mean "allow company.io or everyone", it means only allow company.io. The allowlist layers are AND semantics, the full decision tree is in Sign-in and signup configuration → Signup allowlists.

Invite flows themselves do not check the signup allowlist, but the invitee must still be able to sign in before accepting the invite. If they already have a Concierto account (for example from another workspace), they can accept directly, unaffected by the allowlist; if they have never signed up, the first step of sign-in (requesting a verification code) still passes through the allowlist check, and an email rejected by ALLOW_SIGNUP=false or by ALLOWED_EMAILS / ALLOWED_EMAIL_DOMAINS cannot finish signup, and therefore cannot accept the invite.

Daemon tuning parameters

The daemon runs on the user's local machine, and its config is read from local environment variables too. The common ones:

VariableDefaultDescription
CONCIERTO_SERVER_URLws://localhost:8080/wsServer address (self-host: replace with your domain)
CONCIERTO_DAEMON_HEARTBEAT_INTERVAL15sHeartbeat interval
CONCIERTO_DAEMON_POLL_INTERVAL3sTask polling interval
CONCIERTO_DAEMON_MAX_CONCURRENT_TASKS20Max concurrent tasks
CONCIERTO_<PROVIDER>_PATHmatches the CLI namePath to each AI coding tool's executable (for example CONCIERTO_CLAUDE_PATH)
CONCIERTO_<PROVIDER>_MODELemptyDefault model for each AI coding tool

For a full explanation of how each parameter affects daemon behavior, see Daemon and runtimes.

Frontend access control

VariableDefaultDescription
FRONTEND_ORIGINemptyFrontend address. Invite email links, the CORS allowlist, and the cookie domain are all derived from this. When unset, invite email links fall back to the hosted domain https://concierto.nstack.ai, self-host must set this explicitly
CORS_ALLOWED_ORIGINSemptyAdditional allowed CORS origins (comma-separated)
ALLOWED_ORIGINSemptyWebSocket-specific origin allowlist (comma-separated); when unset, fallback order is CORS_ALLOWED_ORIGINSFRONTEND_ORIGINlocalhost:3000/5173/5174

Leaving FRONTEND_ORIGIN unset creates two silent failures: (1) invite email links point at https://concierto.nstack.ai (the hosted domain), and clicking them doesn't bring users back to your self-hosted instance; (2) WebSocket Origin checks fall back to localhost:3000 / 5173 / 5174, so every WebSocket connection in a production deployment is rejected and the frontend appears to "lose real-time updates."

GitHub integration

The GitHub PR ↔ issue integration needs two variables. Set both to enable Connect GitHub in Settings and accept incoming webhooks.

VariableDefaultDescription
GITHUB_APP_SLUGemptyThe slug of your GitHub App (the tail of https://github.com/apps/<slug>). Drives the Settings → Integrations install button URL
GITHUB_WEBHOOK_SECRETemptyThe Webhook secret you set on the GitHub App. Used for HMAC-SHA256 verification of every pull_request / installation delivery, and as the HMAC key for the setup-callback state token

Behavior when either is unset:

  • Connect GitHub in Settings → Integrations is disabled and shows a "not configured" hint to admins.
  • The /api/webhooks/github endpoint returns 503 github webhooks not configured, Concierto refuses to process events with no secret rather than treating every signature as valid.

Note: GITHUB_WEBHOOK_SECRET is reused as the signing key for the install-flow state token, so operators only need to manage one secret. It is not the GitHub App's Client secret, Client secrets are OAuth-related and not used by this integration. See GitHub integration → Self-host setup for the full walkthrough.

Usage analytics

By default, the server reports to Concierto's official PostHog instance. To opt out, set ANALYTICS_DISABLED=true.

VariableDefaultDescription
ANALYTICS_DISABLEDfalseSet true to disable backend analytics entirely
POSTHOG_API_KEYbuilt-in default keySet when pointing at your own PostHog instance
POSTHOG_HOSThttps://us.i.posthog.comChange to your own host if you self-host PostHog

Next