Skip to content

Environment Variables

Every option a self-hosted Quackback reads from its environment. Copy .env.example (or .env.prod.example for the Docker Compose stack) to .env and fill in your values.

Don't wrap values in quotes and don't put # comments on a value line. Docker's --env-file and Compose's env_file read everything after = literally, so DATABASE_URL="postgresql://..." keeps the quotes and fails with ERR_INVALID_URL.

Required Variables

These variables must be set for Quackback to run. The app checks its configuration at startup and exits with a message naming any missing or invalid variable.

VariableDescriptionExample
DATABASE_URLPostgreSQL connection string. Must be a direct or session-mode connection, not a transaction-mode pooler, because realtime uses LISTEN/NOTIFY.postgresql://quackback:password@localhost:5432/quackback
BASE_URLPublic URL of your instance, used for auth, emails, and OAuth callbackshttps://feedback.example.com
SECRET_KEYAt least 32 characters. Signs sessions and derives encryption keys. Keep it the same across upgrades.Generate with openssl rand -base64 32

PostgreSQL is the only service Quackback needs. The background job queue, caches, rate limits, and realtime events all live in it. See System Requirements.

Server

VariableDefaultDescription
PORT3000Port the server listens on
NODE_ENVproduction in the Docker imageproduction, development, or test
TRUSTED_PROXY_HOPS0Number of reverse proxies in front of the app. See Reverse proxy.
TRUSTED_CLIENT_IP_HEADERunsetName of a header your proxy sets to the client IP, such as x-real-ip or cf-connecting-ip. Takes precedence over TRUSTED_PROXY_HOPS. See Reverse proxy.
TRUSTED_ORIGINSnoneExtra origins allowed to make authenticated requests, comma-separated. BASE_URL is always trusted.
QUACKBACK_COMPRESSIONonSet to off to stop the app compressing responses, for example when your proxy already compresses them
CHAT_TRANSPORT_MODElivelive streams chat over server-sent events. Set to poll to force the widget and portal onto polling if a proxy buffers or drops long-lived streams.
QUACKBACK_CONFIG_FILE/etc/quackback/config.yamlPath of the declarative config file

Reverse proxy

TRUSTED_PROXY_HOPS tells the app how many proxies to look past in X-Forwarded-For to find the client IP used for rate limiting.

SetupValue
Clients connect directly0
One reverse proxy (Nginx, Caddy, Traefik, Cloudflare Tunnel, Railway)1
A CDN in front of a reverse proxy2

Left at 0 behind a proxy, every visitor shares one rate-limit bucket and the app logs a warning. Never set it above 0 if clients can reach the app port directly: they could then choose the IP the app sees.

If your proxy sets one authoritative client-IP header, set TRUSTED_CLIENT_IP_HEADER to its name instead of counting hops, for example x-real-ip with Nginx's proxy_set_header X-Real-IP $remote_addr;, or cf-connecting-ip behind Cloudflare. It's the better choice when requests pass through a number of proxies you don't control. The header is used only when it holds exactly one valid IP address; otherwise the app falls back to TRUSTED_PROXY_HOPS and logs a warning. X-Forwarded-For isn't accepted here. Only name a header your proxy always sets or overwrites, and keep the app port unreachable except through the proxy, or clients can spoof their IP. See Set the trusted proxy hops.

Database

These are optional. The defaults work for most deployments. Tune them only if you've measured a connection-pool bottleneck.

VariableDefaultDescription
DB_POOL_MAX10 (web/all), 20 (worker)Max PostgreSQL connections this process opens (1-100). Worker replicas get a larger pool because they run more concurrent jobs.
DB_IDLE_TIMEOUT20Seconds an idle pooled connection stays open before closing (1-3600)

Server Roles and Background Jobs

Single-container installs can skip this section. The defaults already do what you want. Read Scale with multiple replicas before splitting roles.

VariableDefaultDescription
QUACKBACK_ROLEallWhat this process does. all serves HTTP and runs background jobs. web serves HTTP and enqueues jobs without running them. worker runs background jobs and serves HTTP only for health probes. migrator is reserved for multi-workspace hosting and isn't used by a single install; to run migrations on their own, use the migrate-only container. An unrecognised value stops the process from starting.

Background jobs run on a queue table in PostgreSQL. These settings tune the job worker on worker and all processes:

VariableDefaultDescription
JOB_POLL_INTERVAL_MS1000How often the worker checks for jobs when no wake-up notification arrives (50-600000)
JOB_BATCH_SIZE5Max jobs claimed from one queue in a single pass (1-100)
JOB_MAX_CONCURRENCYsum of all queues' limitsMax jobs running at once in this process (1-512)
JOB_REAP_INTERVAL_MS15000How often jobs whose worker died are made claimable again (500-3600000)
JOB_PRUNE_INTERVAL_MS3600000How often finished jobs past retention are deleted (1000-86400000)
JOB_RETENTION_MS604800000 (7 days)How long finished and failed jobs are kept

Email Configuration

Email delivers sign-in codes, invitations, and notifications. Configure exactly one sending provider: SMTP, Amazon SES, or Resend. If more than one is configured, the app refuses to start and names the conflicting variables. See Email.

If no provider is configured, sign-in codes and other emails are printed to the server log. This is useful for local development.

VariableDescriptionExample
EMAIL_FROMSender address. Required with any provider.Quackback <feedback@example.com>

SMTP

VariableDescriptionExample
EMAIL_SMTP_HOSTSMTP server hostname. Setting it selects SMTP.smtp.sendgrid.net
EMAIL_SMTP_PORTSMTP port587
EMAIL_SMTP_USERSMTP usernameapikey
EMAIL_SMTP_PASSSMTP passwordSG.xxxx
EMAIL_SMTP_SECURESet to true for implicit TLS on port 465true

Amazon SES

VariableDescriptionExample
EMAIL_SES_ACCESS_KEY_IDAccess key of an IAM user with ses:SendEmail. Required.AKIA...
EMAIL_SES_SECRET_ACCESS_KEYMatching secret. Required.
EMAIL_SES_REGIONRegion your sending identity is verified in. Required, with no default: identities are regional and a wrong region rejects every send.us-east-1
EMAIL_SES_IDENTITY_ACCESS_KEY_IDOptional. A separate key used only to verify a custom sending domain. Grant it ses:CreateEmailIdentity, ses:GetEmailIdentity, and ses:PutEmailIdentityMailFromAttributes, never ses:DeleteEmailIdentity.
EMAIL_SES_IDENTITY_SECRET_ACCESS_KEYMatching secret for the identity key

Resend

VariableDescriptionExample
EMAIL_RESEND_API_KEYResend API key. RESEND_API_KEY is accepted as an alias.re_xxxxxxxxxxxx

The Resend key also fetches inbound message bodies when you receive mail through Resend. To keep it for receiving only while SMTP or SES sends, set EMAIL_INBOUND_PROVIDER=resend (see below).

Inbound Email

Lets a customer's email reply thread back into their conversation. There are two ways to receive: a provider webhook or IMAP polling.

Provider webhook

VariableDescription
EMAIL_INBOUND_DOMAINReceiving domain. Agent replies use reply+<conversationId>@<domain>. Exactly one domain.
EMAIL_INBOUND_SIGNING_SECRETThe provider's webhook signing secret (Svix whsec_...)
EMAIL_INBOUND_EXTRA_DOMAINSOptional. More domains to keep accepting replies on, comma- or space-separated. Use it when you change EMAIL_INBOUND_DOMAIN: list the old domain here so reply addresses already sent keep working.
EMAIL_INBOUND_PROVIDERSet to resend when you receive through Resend but send through SMTP or SES. The Resend key is then used for inbound only.

Set EMAIL_INBOUND_DOMAIN and EMAIL_INBOUND_SIGNING_SECRET together, and point the provider's inbound webhook at <BASE_URL>/api/chat/email/inbound.

IMAP polling

Needs no public webhook endpoint. A background worker polls the mailbox about every 60 seconds. See Receive email over IMAP.

VariableDefaultDescription
EMAIL_INBOUND_PROVIDERunsetSet to imap to enable the poller. Leave unset to disable it.
IMAP_HOSTrequiredIMAP server hostname
IMAP_PORT993 (TLS) or 143 (plaintext)IMAP server port
IMAP_USERrequiredMailbox username
IMAP_PASSWORDrequiredMailbox password
IMAP_TLStrueSet to false for a plaintext connection
IMAP_MAILBOXINBOXMailbox to poll

EMAIL_INBOUND_PROVIDER=imap, IMAP_HOST, IMAP_USER, and IMAP_PASSWORD are required together. The poller never connects unless all four are set.

Sign-in Providers and Integrations

GitHub, Google, and other sign-in providers, custom OIDC, and integrations such as Slack, Linear, and Microsoft Teams are configured in the admin UI (Admin → Settings → Access & Security and Admin → Settings → Integrations). Their credentials are stored encrypted in the database, not in environment variables.

AI

AI powers post summaries, duplicate detection, feedback extraction, help center answers, and Quinn. AI is off unless you set the key, the endpoint, and a model. Quackback works with any OpenAI-compatible endpoint but never assumes one.

VariableDescriptionExample
OPENAI_API_KEYAPI key for your AI endpointsk-...
OPENAI_BASE_URLOpenAI-compatible endpoint. Required, with no default.https://api.openai.com/v1
AI_CHAT_MODELDefault model for every chat featuregpt-4o-mini
AI_EMBEDDING_MODELModel for embeddings (duplicate detection, semantic search)text-embedding-3-small

Per-feature models

Each override falls back to AI_CHAT_MODEL when unset. Set one to off to turn just that feature off while the rest keep working. AI_CHAT_MODEL=off or AI_EMBEDDING_MODEL=off turns off the whole role.

VariableFeature
AI_SUMMARY_MODELPost summaries
AI_SENTIMENT_MODELSentiment analysis
AI_EXTRACTION_MODELFeedback extraction into suggestions
AI_QUALITY_GATE_MODELSuggestion quality gate
AI_INTERPRETATION_MODELFeedback interpretation
AI_MERGE_MODELDuplicate merge verification
AI_HELP_CENTER_MODELHelp center Ask AI answers
AI_HELP_CENTER_TRANSLATE_MODELHelp center article translation
AI_ASSISTANT_MODELQuinn
AI_INBOX_TRANSLATION_MODELInbox message translation
AI_CLASSIFICATION_MODELConversation attribute classification

Use model ids your endpoint accepts. Gateways usually want provider-prefixed ids such as google/gemini-3.1-flash-lite-preview.

Model behaviour

VariableDefaultDescription
AI_ASSISTANT_VISIONdetected from the model nameon or off. Whether Quinn may send images to the model. Set it when a proxied or renamed model hides its capabilities.
AI_REQUIRE_PARAMETERStrue on OpenRouterOpenRouter only. Routes structured-output requests to providers that enforce the JSON schema. Set false only if your model has no such provider.
AI_REASONING_EXCLUDEfalseOpenRouter only. Set true to hide reasoning tokens on structured streams, for reasoning models.
AI_REASONING_EFFORTunsetOpenRouter only. minimal, low, medium, high, or max, passed to models that support a reasoning effort.
AI_COMBINED_TOOLS_AND_SCHEMAfalseSet true if your model can return tool calls and the final JSON in one request. Unset splits them into two calls.

Before v0.12.0, OPENAI_API_KEY alone enabled AI with an implicit OpenAI endpoint and built-in model names. If you upgraded and AI features turned off, add OPENAI_BASE_URL, AI_CHAT_MODEL, and AI_EMBEDDING_MODEL.

File Storage

S3-compatible storage for uploads: images, attachments, logos, and videos. Works with AWS S3, Cloudflare R2, Backblaze B2, Railway buckets, the bundled PGSTY Silo, and other S3-compatible services. Without it, uploads are disabled.

VariableDescriptionExample
S3_ENDPOINTS3 endpoint URL. Leave empty for AWS S3.http://localhost:9000
S3_BUCKETBucket namequackback
S3_REGIONRegionus-east-1
S3_ACCESS_KEY_IDAccess key
S3_SECRET_ACCESS_KEYSecret key
S3_FORCE_PATH_STYLEUse path-style URLs. Required for Silo, MinIO, and R2.true
S3_PROXYSet true to stream uploads and downloads through the app instead of redirecting the browser to presigned URLs. Use it when browsers can't reach the bucket, as in the Docker Compose stack.false
S3_PUBLIC_URLOptional. Doesn't change the URLs of new files. Set it only if content from an earlier version links to files at a public bucket or CDN URL: links under that base are then still recognised as this install's own files.https://cdn.example.com
USER_CONTENT_URLOptional. A separate origin pointed at this app. Attachment links then load from that host, away from the app's cookies. A bare origin with no path.https://files.example.com

Files are always served through <BASE_URL>/api/storage, which works with private buckets. New files are stored under w/<workspace id>/ in the bucket. Files from before that layout are copied into it in the background on first start, and the originals are kept.

Telemetry

Quackback sends one anonymous snapshot a day so the project can see which versions, setups, and features are in use. It never includes names, emails, URLs, hostnames, content, or exact counts. Counts are reported in bands such as 11-50. See Telemetry for every field.

VariableDescriptionExample
DISABLE_TELEMETRYSet to true to turn the daily snapshot offtrue

Product Analytics

Optional. Sends admin-app usage (pageviews, clicks, and masked session replay) to your own PostHog project. Off unless POSTHOG_KEY is set. Portal, widget, and help center visitors are never tracked, and neither is a browser that sends Do Not Track or Global Privacy Control.

VariableDefaultDescription
POSTHOG_KEYunsetYour PostHog project key. Setting it turns product analytics on.
POSTHOG_HOSThttps://us.i.posthog.comIngestion host, or your own reverse proxy in front of PostHog
POSTHOG_UI_HOSTunsetThe PostHog app URL, needed for toolbar links when POSTHOG_HOST is a proxy
POSTHOG_SESSION_RECORDINGtrueSet false to turn off session replay

Logging

The server writes structured JSON logs to stdout, one object per line, ready for any log shipper (Grafana Alloy, Promtail, Fluent Bit, Vector).

VariableDefaultDescription
LOG_LEVELinfo in production, debug otherwisetrace, debug, info, warn, error, fatal, or silent
OTEL_SERVICE_NAMEquackbackThe service_name field on every log line

Docker Startup

These are read by the container's entrypoint script. They apply to any Docker deployment (Compose or docker run).

VariableDefaultDescription
SKIP_MIGRATIONSfalseSet to true to skip the startup migration, for example with several replicas or a Kubernetes pre-upgrade hook. Run migrations with docker run --entrypoint bun <image> /app/migrate.mjs.
SEED_DATABASEfalseSet to true to load demo data on startup. Don't enable this in production.

Docker Compose Variables

These configure the Compose stack (docker-compose.prod.yml), not the application. Compose uses them to name containers and to build values like DATABASE_URL from your secrets. See Deploy with Docker.

VariableDefaultDescription
QUACKBACK_TAGlatestImage tag to run. Pin a release in production.
APP_PORT3000Host port the app is published on. The container always listens on 3000.
COMPOSE_PROJECT_NAMEquackbackName Compose uses for containers, network, and volumes
POSTGRES_USERrequiredBundled PostgreSQL username
POSTGRES_PASSWORDrequiredBundled PostgreSQL password
POSTGRES_DBquackbackBundled PostgreSQL database name
MINIO_ROOT_USERrequiredBundled object storage root user. Doubles as the app's S3_ACCESS_KEY_ID.
MINIO_ROOT_PASSWORDrequiredBundled object storage root password. Doubles as the app's S3_SECRET_ACCESS_KEY.
SILO_IMAGEdigest-pinned pgsty/siloFull image reference for the bundled storage server, for a mirror or another release
SILO_CLIENT_IMAGEdigest-pinned pgsty/mcFull image reference for the client that creates the bucket

MINIO_IMAGE_TAG and MC_IMAGE_TAG from 0.13 are ignored. If your .env sets them, remove them.

Complete Example

# Required
DATABASE_URL=postgresql://quackback:password@localhost:5432/quackback
SECRET_KEY=your-32-character-minimum-secret-key-here
BASE_URL=https://feedback.yourcompany.com
 
# One reverse proxy in front of the app
TRUSTED_PROXY_HOPS=1
 
# Email: configure exactly one provider
EMAIL_SMTP_HOST=smtp.sendgrid.net
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=apikey
EMAIL_SMTP_PASS=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
EMAIL_FROM=Quackback <feedback@yourcompany.com>
 
# File storage
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_BUCKET=quackback
S3_REGION=auto
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_FORCE_PATH_STYLE=true
 
# AI (optional)
# OPENAI_API_KEY=sk-...
# OPENAI_BASE_URL=https://api.openai.com/v1
# AI_CHAT_MODEL=gpt-4o-mini
# AI_EMBEDDING_MODEL=text-embedding-3-small
 
# Telemetry (optional)
# DISABLE_TELEMETRY=true