AnswerLoopsAnswerLoops Docs
Self-Hosting

Environment Variables

Every environment variable AnswerLoops uses, with defaults and requirements.

Copy .env.local.example to .env.local and fill in the values below.

AnswerLoops uses Auth.js v5. The required env vars are AUTH_URL and AUTH_SECRET. If you see NEXTAUTH_URL or NEXTAUTH_SECRET in older docs or guides, those are the v4 names — they will not work here.

Core (required)

VariableDescriptionExample
DATABASE_URLPostgreSQL connection stringpostgresql://community:community@postgres:5432/community
DIRECT_DATABASE_URLNon-pooled connection string, used only for LISTEN/NOTIFY (config hot-reload, live team updates). Required if DATABASE_URL goes through a connection pooler (Neon's -pooler endpoint, PgBouncer, Supabase's pooled port) — pooled connections don't reliably deliver NOTIFY to a LISTENer, since the pooler can swap the physical backend between statements. Falls back to DATABASE_URL if unset, which is correct for a plain unpooled Postgres instance (e.g. the local docker compose Postgres) but silently breaks hot-reload on a pooled provider.Neon: the same connection string with -pooler removed from the hostname (Neon dashboard → Connection Details → toggle "Pooled connection" off)
AUTH_URLFull public URL of your deploymenthttp://localhost:3000 or https://app.example.com
AUTH_SECRETRandom 32-byte hex — signs JWT sessionsopenssl rand -hex 32
ENCRYPTION_KEYRandom 32-byte hex — encrypts stored API keys and bot tokens at restopenssl rand -hex 32

Networking (optional)

VariableDescriptionExample
TRUST_PROXY_HOPSHow many proxies sit between the public internet and the app. Defaults to 11

Rate limiting needs the real client IP, and it reads that from x-forwarded-for. That header is a list each proxy appends to, so its leftmost entry is whatever the caller claimed — trusting it lets anyone mint a fresh rate-limit bucket per request just by rotating a header value. AnswerLoops instead counts in from the right by TRUST_PROXY_HOPS, reading the entry your own infrastructure appended.

Multi-domain (optional)

Only relevant if you're running the dashboard on a separate subdomain from your marketing/root domain (e.g. app.example.com alongside example.com) while both point at the same deployment. Most self-hosted setups use a single domain and can skip this section entirely.

VariableDescriptionExample
NEXT_PUBLIC_APP_URLBase URL of the dashboard subdomain. When set, marketing-page CTAs link there instead of a relative /dashboard path. Leave unset for a single-domain deployment.https://app.example.com
AUTH_COOKIE_DOMAINShares the session cookie across every subdomain of your apex domain. Required alongside NEXT_PUBLIC_APP_URL — without it, a user signed in on the root domain appears logged out the moment they land on the app subdomain, since a session cookie is host-only by default. Leave unset for a single-domain deployment..example.com

Set this to the actual number of proxies in front of the app. Too high and you read a value the client controls, which defeats per-IP limiting. Too low and everyone behind your edge shares one bucket, which throttles legitimate traffic. The default of 1 is correct for a single load balancer or CDN (Railway, Fly, a lone nginx). Add one for each additional layer — Cloudflare in front of Railway is 2.

Cloudflare's cf-connecting-ip header is used when present and takes precedence, since Cloudflare overwrites rather than appends it.

VariableDescriptionExample
ORIGIN_VERIFY_SECRETOptional secret that locks the proxy-supplied client-IP header's trust to traffic that actually passed through your edge proxy/CDNopenssl rand -hex 32

The proxy-supplied client-IP header (cf-connecting-ip or equivalent) is only spoof-proof if the origin is unreachable except through your edge proxy. If your origin is still reachable directly, anyone can set that header themselves and mint a fresh rate-limit bucket per request, bypassing both the per-IP limiter and your edge's own WAF/DDoS layer.

To close this:

  1. Generate a secret: openssl rand -hex 32.
  2. Set ORIGIN_VERIFY_SECRET to that value in your deployment's environment.
  3. On your CDN/edge proxy, add a rule (most offer a "modify request header" or equivalent feature) that runs for all traffic and sets a request header x-origin-verify to the same secret value.
  4. Confirm requests hitting your origin without that header now get a 403 Forbidden from /api/mcp, /api/agent/*, and /api/widget/chat.

Until this is configured, the proxy-supplied client-IP header is trusted unconditionally — the app has no way to tell an edge-routed request from a direct one. Set ORIGIN_VERIFY_SECRET on any deployment sitting behind a CDN or edge proxy.

OAuth (at least one required)

Login uses GitHub, Discord, or Google OAuth. Configure at least one.

VariableDescription
AUTH_GITHUB_IDGitHub OAuth App client ID
AUTH_GITHUB_SECRETGitHub OAuth App client secret
AUTH_DISCORD_IDDiscord OAuth2 client ID
AUTH_DISCORD_SECRETDiscord OAuth2 client secret
AUTH_GOOGLE_IDGoogle OAuth client ID
AUTH_GOOGLE_SECRETGoogle OAuth client secret

Callback URLs to register:

  • GitHub: {AUTH_URL}/api/auth/callback/github
  • Discord: {AUTH_URL}/api/auth/callback/discord
  • Google: {AUTH_URL}/api/auth/callback/google

Per-org AI keys set in Settings → AI Model override these env vars for that org. The platform key is the fallback when no org key is set.

VariableProvider
OPENAI_API_KEYOpenAI (covers all features including embeddings)
ANTHROPIC_API_KEYAnthropic platform default
GOOGLE_GENERATIVE_AI_API_KEYGoogle Gemini platform default
GROQ_API_KEYGroq platform default
MISTRAL_API_KEYMistral platform default

Discord (optional)

VariableDescription
DISCORD_TOKENPlatform shared bot token. Required for 1-click OAuth mode and for the channel picker in Settings.
DISCORD_CLIENT_IDApplication ID — enables the "Add to Discord" OAuth flow. Register callback: {AUTH_URL}/api/discord/callback
DISCORD_CLIENT_SECRETOAuth2 client secret from Discord Developer Portal → OAuth2 tab
DISCORD_APPLICATION_IDApplication ID for slash command registration (same value as DISCORD_CLIENT_ID)
DISCORD_GUILD_IDGuild ID for guild-scoped slash command registration during dev (instant vs. global's 1-hour delay)
BOT_SECRETShared secret between the bot service and /api/ingest. Auto-generated per-org via Settings; this is the env fallback.
BOT_TARGET_URLURL of the app service as seen from the bot container. No trailing slash or period. Default: http://localhost:3000

DISCORD_TOKEN must be set on both the app service and the bot service. The app service uses it to power the channel picker in Settings → Discord. The bot service uses it to connect to Discord's gateway. If they reference different Discord applications, the bot joins a different server than users authorized.

Slack (optional)

VariableDescription
SLACK_CLIENT_IDOAuth app Client ID. Enables 1-click "Add to Slack" install.
SLACK_CLIENT_SECRETOAuth app Client Secret. Required with SLACK_CLIENT_ID.
SLACK_SIGNING_SECRETSigning secret — verifies Events API webhook payloads.
SLACK_POLL_INTERVAL_SECONDSPoll interval in polling mode. Default: 60. Minimum: 30.

Add {AUTH_URL}/api/slack/callback to your Slack app's OAuth Redirect URLs.

Google Chat (optional)

VariableDescription
GOOGLE_CHAT_SERVICE_ACCOUNT_JSONFull service-account JSON key, as a single-line string. Used to authenticate outgoing replies to the Chat API.
GOOGLE_CHAT_ENDPOINT_URLPublic HTTP endpoint URL, e.g. https://{YOUR_DOMAIN}/api/google-chat/events. Must exactly match the endpoint URL configured in the Chat app — Google verifies each request's token audience against this value.

See Google Chat App Setup for the one-time Google Cloud configuration these values come from.

GitHub App (optional)

VariableDescription
GITHUB_APP_IDApp ID from GitHub App settings page
GITHUB_APP_PRIVATE_KEYBase64-encoded PEM: base64 -i your-app.pem | tr -d '\n'
GITHUB_WEBHOOK_SECRETWebhook secret. Generate with openssl rand -hex 32 and paste without the trailing newline.
GITHUB_APP_SLUGURL-safe app name (slug) — shown in your GitHub App's URL

Register these URLs in your GitHub App:

  • Callback URL: {AUTH_URL}/api/github/callback
  • Webhook URL: {AUTH_URL}/api/github/webhook

Subscribe to events: Issues, Issue comments, Discussions, Discussion comments, Push.

Telegram (optional)

VariableDescription
TELEGRAM_BOT_TOKENBot token from @BotFather. Fallback when no org token is saved.

After deploying, register the webhook: POST {AUTH_URL}/api/telegram/register (or click Register webhook in Settings → Telegram).

Email notifications — Resend (optional)

VariableDescription
RESEND_API_KEYAPI key from resend.com. Enables email alerts (new ticket, SLA breach, resolved).
RESEND_FROMVerified sender address e.g. [email protected]
RESEND_WAITLIST_FROMFrom address for waitlist confirmation emails. Falls back to RESEND_FROM.
RESEND_WEBHOOK_SECRETSvix signing secret (whsec_...) from your Resend inbound webhook config — required to offer customers the zero-setup email channel (see Email Channel Setup)
EMAIL_INBOUND_DOMAINDomain used when generating each org's platform-hosted inbound address (defaults to inbox.answerloops.app) — self-hosters should set this to a domain they control

If absent, email notifications skip silently — nothing breaks. Without RESEND_WEBHOOK_SECRET, the platform-hosted email path is unavailable but the BYO-provider path still works.

URL ingest — Firecrawl (optional)

VariableDescription
FIRECRAWL_API_KEYAPI key from firecrawl.dev. Enables Settings → Import from URL to crawl docs sites into the KB.

Web push notifications (optional)

VariableDescription
VAPID_PUBLIC_KEYVAPID public key
VAPID_PRIVATE_KEYVAPID private key
VAPID_EMAILmailto:[email protected] — identifies your push endpoint

Generate keys:

pnpm dlx web-push generate-vapid-keys

Billing / deployment mode

VariableDescriptionExample
DEPLOYMENT_MODESet to cloud only on AnswerLoops' own managed SaaS. Leave unset (or anything other than cloud) on a self-hosted deployment.cloud
STRIPE_SECRET_KEYOnly relevant when DEPLOYMENT_MODE=cloud. Not needed for self-hosting.

Self-hosted deployments (the default — DEPLOYMENT_MODE unset) are never metered and every plan-gated feature (Discord/Slack integrations, CSAT scoring, simulation/dry-run mode, knowledge gap dashboard, custom AI model config, etc.) is unlocked unconditionally. You're already bringing your own AI provider, database, and hosting — there's no usage of yours for AnswerLoops to meter or restrict.

Don't set DEPLOYMENT_MODE=cloud on a self-hosted instance — it opts the deployment into plan-tier gating with no Stripe subscription behind it, which locks every gated feature instead of unlocking them.

Access control (optional)

VariableDescription
ALLOWED_EMAILSComma-separated email addresses allowed to log in. Leave empty for open access. Blocked users see an invite-only error at /login?error=AccessDenied.

On this page