Environment variables
Every environment variable a self-hosted Quackback server reads: required settings, trusted proxy and client-IP header, email, inbound email, AI, storage, telemetry, logging, and Docker.
Every option a self-hosted Quackback server reads from its environment. Copy .env.example (or .env.prod.example for the Docker Compose stack) to .env and fill in your values. This reference is for the open-source edition; Quackback Cloud is configured for you.
Tip: Don't wrap values in quotes and don't put
# commentson a value line. Docker's--env-fileand Compose'senv_fileread everything after=literally, soDATABASE_URL="postgresql://..."keeps the quotes and fails withERR_INVALID_URL.
Required variables
Warning: These 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.
Variable | Description | Example |
|---|---|---|
| PostgreSQL connection string. Must be a direct or session-mode connection, not a transaction-mode pooler, because realtime uses |
|
| Public URL of your instance, used for auth, emails, and OAuth callbacks |
|
| At least 32 characters. Signs sessions and derives encryption keys. Keep it the same across upgrades. | Generate with |
PostgreSQL is the only service Quackback needs. The background job queue, caches, rate limits, and realtime events all live in it. There is no REDIS_URL: if an old .env still has one, it is ignored and you can delete it. See System requirements.
Server
Variable | Default | Description |
|---|---|---|
|
| Port the server listens on |
|
|
|
|
| Number of reverse proxies in front of the app. See Reverse proxy. |
| unset | Name of a header your proxy sets to the client IP, such as |
| none | Extra origins allowed to make authenticated requests, comma-separated. |
| on | Set to |
|
|
|
|
| Path 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.
Setup | Value |
|---|---|
Clients connect directly |
|
One reverse proxy (Nginx, Caddy, Traefik, Cloudflare Tunnel, Railway) |
|
A CDN in front of a reverse proxy |
|
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_HOPSand logs a warning. X-Forwarded-Forisn't accepted here (useTRUSTED_PROXY_HOPSfor it).- 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.
Database
Note: These are optional. The defaults work for most deployments. Tune them only if you've measured a connection-pool bottleneck.
Variable | Default | Description |
|---|---|---|
|
| Max PostgreSQL connections this process opens (1-100). Worker replicas get a larger pool because they run more concurrent jobs. |
|
| Seconds an idle pooled connection stays open before closing (1-3600) |
Server roles and background jobs
Tip: Single-container installs can skip this section. The defaults already do what you want. Read Scale with multiple replicas before splitting roles.
Variable | Default | Description |
|---|---|---|
|
| What this process does. |
Background jobs run on a queue table in PostgreSQL. These settings tune the job worker on worker and all processes:
Variable | Default | Description |
|---|---|---|
|
| How often the worker checks for jobs when no wake-up notification arrives (50-600000) |
|
| Max jobs claimed from one queue in a single pass (1-100) |
| sum of all queues' limits | Max jobs running at once in this process (1-512) |
|
| How often jobs whose worker died are made claimable again (500-3600000) |
|
| How often finished jobs past retention are deleted (1000-86400000) |
|
| How long finished and failed jobs are kept |
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 Configure email delivery.
Tip: If no provider is configured, sign-in codes and other emails are printed to the server log. This is useful for local testing.
Variable | Description | Example |
|---|---|---|
| Sender address. Required with any provider. |
|
SMTP
Variable | Description | Example |
|---|---|---|
| SMTP server hostname. Setting it selects SMTP. |
|
| SMTP port |
|
| SMTP username |
|
| SMTP password | |
| Set to |
|
Amazon SES
Variable | Description | Example |
|---|---|---|
| Access key of an IAM user with |
|
| Matching secret. Required. | |
| Region your sending identity is verified in. Required, with no default: identities are regional and a wrong region rejects every send. |
|
| Optional. A separate key used only to verify a custom sending domain. Grant it | |
| Matching secret for the identity key |
Resend
Variable | Description | Example |
|---|---|---|
| Resend API key. |
|
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
Variable | Description |
|---|---|
| Receiving domain. Agent replies use |
| The provider's webhook signing secret (Svix |
| Optional. More domains to keep accepting replies on, comma- or space-separated. When you change |
| Set to |
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.
Variable | Default | Description |
|---|---|---|
| unset | Set to |
| required | IMAP server hostname |
|
| IMAP server port |
| required | Mailbox username |
| required | Mailbox password |
|
| Set to |
|
| Mailbox to poll |
Note:
EMAIL_INBOUND_PROVIDER=imap,IMAP_HOST,IMAP_USER, andIMAP_PASSWORDare 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), not in environment variables. Their credentials are stored encrypted in the database. See Set up OAuth providers.
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, including local model servers, but never assumes one. Data for AI features goes only to the endpoint you configure.
Variable | Description | Example |
|---|---|---|
| API key for your AI endpoint |
|
| OpenAI-compatible endpoint. Required, with no default. |
|
| Default model for every chat feature |
|
| Model for embeddings (duplicate detection, semantic search) |
|
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.
Variable | Feature |
|---|---|
| Post summaries |
| Sentiment analysis |
| Feedback extraction into suggestions |
| Suggestion quality gate |
| Feedback interpretation |
| Duplicate merge verification |
| Help center Ask AI answers |
| Help center article translation |
| Quinn |
| Inbox message translation |
| Conversation 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
Variable | Default | Description |
|---|---|---|
| detected from the model name |
|
|
| OpenRouter only. Routes structured-output requests to providers that enforce the JSON schema. Set |
|
| OpenRouter only. Set |
| unset | OpenRouter only. |
|
| Set |
Note: Before v0.12.0,
OPENAI_API_KEYalone enabled AI with an implicit OpenAI endpoint and built-in model names. If you upgraded and AI features turned off, addOPENAI_BASE_URL,AI_CHAT_MODEL, andAI_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.
Variable | Description | Example |
|---|---|---|
| S3 endpoint URL. Leave empty for AWS S3. |
|
| Bucket name |
|
| Region |
|
| Access key | |
| Secret key | |
| Use path-style URLs. Required for Silo, MinIO, and R2. |
|
| Set |
|
| Optional. 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. |
|
| Optional. 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. |
|
Files are always served through <BASE_URL>/api/storage, which works with private buckets. Files uploaded by earlier versions are copied into the current storage layout 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.
Variable | Description | Example |
|---|---|---|
| Set to |
|
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.
Variable | Default | Description |
|---|---|---|
| unset | Your PostHog project key. Setting it turns product analytics on. |
|
| Ingestion host, or your own reverse proxy in front of PostHog |
| unset | The PostHog app URL, needed for toolbar links when |
|
| Set |
Logging
The server writes structured JSON logs to stdout, one object per line, ready for any log shipper (Grafana Alloy, Promtail, Fluent Bit, Vector).
Variable | Default | Description |
|---|---|---|
|
|
|
|
| The |
Docker startup
Note: These are read by the container's entrypoint script. They apply to any Docker deployment (Compose or
docker run).
Variable | Default | Description |
|---|---|---|
|
| Set to |
|
| Set to |
Docker Compose variables
Note: These configure the Compose stack (
docker-compose.prod.yml), not the application. Compose uses them to name containers and to build values likeDATABASE_URLfrom your secrets. See Deploy with Docker.
Variable | Default | Description |
|---|---|---|
|
| Image tag to run. Pin a release (for example |
|
| Host port the app is published on. The container always listens on 3000. |
|
| Name Compose uses for containers, network, and volumes |
| required | Bundled PostgreSQL username |
| required | Bundled PostgreSQL password |
|
| Bundled PostgreSQL database name |
| required | Bundled object storage root user. Doubles as the app's |
| required | Bundled object storage root password. Doubles as the app's |
| digest-pinned | Full image reference for the bundled storage server, for a mirror or another release |
| digest-pinned | Full image reference for the client that creates the bucket |
Note:
MINIO_IMAGE_TAGandMC_IMAGE_TAGfrom 0.13 are ignored. If your.envsets 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.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=your-username
EMAIL_SMTP_PASS=your-password
EMAIL_FROM=Quackback <[email protected]>
# 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=trueRelated articles
Was this helpful?
Your feedback shapes what we write next.
