---
title: Environment variables
description: Every environment variable the Feeblo server and dashboard read, grouped by concern with defaults and which ones are required.
---

The server reads its configuration from the process environment at boot, and the
dashboard reads a smaller subset through its own Astro env schema. This page covers
every variable in `.env.example`, grouped by concern. Each row states whether the code
treats the variable as required, what default applies, and what breaks when you leave
it out.

With one exception, a required mark means the process fails to start without the value.
The Media group resolves its config lazily, so missing values there surface on the
first upload instead of at boot.

:::warning[Docker Compose forwards a small subset]
`docker compose` does not pass your whole `.env` file into containers. The server
service forwards the core URL variables, the database and Redis URLs, the auth
encryption key, the trusted origins, the release label, the two email provider feedback
variables, and the media group. The web service forwards the three URL variables plus
`HOST` and `PORT`. Everything else on this page you must add yourself to the matching
service's `environment` block in `docker-compose.yml`.

See [Deploy with Docker](/self-hosting/docker) for the full compose walkthrough.
:::

## Core

| Variable            | Required | Default                                  | What breaks without it |
| ------------------- | -------- | ---------------------------------------- | ---------------------- |
| `DATABASE_URL`      | Yes      | None (compose: `postgres://feeblo:password@pg:5432/feeblo`) | Postgres DSN for the server and the migration runner. The server config layer fails at boot. See [Database](/self-hosting/database). |
| `REDIS_URL`         | No       | In-memory rate-limiter store             | Redis DSN, optionally with username, password, and db index. Set it when running more than one server so the rate-limiter store is shared; without it every instance limits independently. The compose network defaults to `redis://redis:6379/0`. |
| `APP_URL`           | Yes      | None (compose: `http://localhost:3001`)  | Public URL of the dashboard. Auth redirects, cookies, and trusted origins derive from it. It must match what browsers hit, or sign-in breaks in confusing ways. |
| `API_URL`           | Yes      | None (compose: `http://localhost:3000`)  | Public URL of the API. The Slack, Discord, SES, and Polar webhook URLs all derive from it. |
| `APP_ROOT_DOMAIN`   | Yes      | None (compose: `localhost`)              | Root domain your public boards live on. Boards run on arbitrary subdomains of it, so a DNS wildcard must cover it. |
| `APP_RELEASE`       | No       | `dev`                                    | Release label. The server returns it from `/health` and the dashboard displays it. Point it at a git SHA or tag to identify a running deployment. |
| `NO_INDEX`          | No       | Unset                                    | Any non-empty value adds `noindex` to dashboard and board pages. An empty value disables the behavior. |

## Auth

| Variable               | Required | Default          | Notes |
| ---------------------- | -------- | ---------------- | ----- |
| `AUTH_ENCRYPTION_KEY`  | Yes      | None             | At-rest key for auth tokens, verification codes, and email-subscription tokens. When the integration encryption key is unset it also encrypts integration credentials, and then it must be at least 32 bytes. Missing it, and the server refuses to start with a missing-key error; compose additionally fails fast at `docker compose up`. Generate one with `openssl rand -hex 32`. |
| `AUTH_TRUSTED_ORIGINS` | No       | The app and API URLs, plus `*.localhost:3001` in development or a `{scheme}//*.{root domain}` wildcard in production | Comma-separated browser origins allowed to call the API (better-auth `trustedOrigins`). Public board subdomains need a wildcard for the root domain, for example `*.feeblo.com`; scheme-less patterns match both http and https hosts. |
| `GITHUB_CLIENT_ID`     | No       | None             | GitHub sign-in. Set it together with the client secret below. |
| `GITHUB_CLIENT_SECRET` | No       | None             | GitHub OAuth secret. |
| `GOOGLE_CLIENT_ID`     | No       | None             | Google sign-in, paired with the secret below. |
| `GOOGLE_CLIENT_SECRET` | No       | None             | Google OAuth secret. |
| `GITHUB_EMULATOR_URL`  | No       | None             | Points GitHub sign-in at a local OAuth emulator (vercel-labs/emulate) instead of the real provider, for example `http://localhost:4000`. Development only. |
| `GOOGLE_EMULATOR_URL`  | No       | None             | Same hook for Google, for example `http://localhost:4001`. |

:::warning[The auth key must stay stable]
This key encrypts session material. Rotate it and every existing session, verification
code, and email-subscription token stops decrypting. Generate one key, store it in
your secret manager, and never change it during normal operation.
:::

See [OAuth sign-in](/self-hosting/oauth) for registering the provider apps.

## Email

The mailer builds its SMTP transport from the credentials below. Defaults come from
`packages/transactional/src/config.ts`; several differ from the sample values in
`.env.example`.

| Variable                          | Required | Default               | Notes |
| --------------------------------- | -------- | --------------------- | ----- |
| `SMTP_TRANSPORT`                  | No       | `smtp-auth`           | Documented options are `smtp-auth` (default), `smtp-api`, and `resend`. The current mailer does not read this variable yet; it always uses the credentials below. |
| `SMTP_HOST`                       | No       | `127.0.0.1`           | SMTP server host. |
| `SMTP_PORT`                       | No       | `2500`                | SMTP port. The sample value in `.env.example` is `1025`. |
| `SMTP_USERNAME`                   | No       | None                 | Auth user passed to nodemailer. Omit for an open relay. |
| `SMTP_PASSWORD`                   | No       | None                 | Auth secret, paired with the username. |
| `SMTP_SECURE`                     | No       | `false`              | Force TLS on the connection. |
| `SMTP_UNSAFE_IGNORE_TLS`          | No       | `false`              | When `true` and force-TLS is off, TLS is skipped even if the server advertises STARTTLS. Meant only for broken local relays. |
| `SMTP_FROM_NAME`                  | No       | None                 | Display name for the from address. `.env.example` documents it, but the current mailer does not read it yet. |
| `SMTP_FROM_ADDRESS`               | No       | `hello@feeblo.com`   | Sender address. `.env.example` marks it required, but the code falls back to this value, so set it explicitly to a real address in production. |
| `SMTP_PERSONAL_FROM_ADDRESS`      | No       | Falls back to the default from address | Dedicated sender for personal lifecycle mail (user onboarding, user feedback). |
| `SMTP_SERVICE`                    | No       | None                 | Nodemailer well-known service name, passed through to the transport. |
| `EMAIL_PROVIDER_WEBHOOK_TOKEN`    | No       | None                 | The token alone activates the Amazon SES feedback webhook at the API path `/email-provider/ses/<token>`. Unset, and the route stays inert (404). Generate a dedicated one with `openssl rand -hex 32`; do not reuse the auth at-rest key. |
| `EMAIL_PROVIDER_SNS_TOPIC_ARN`    | No       | None                 | ARN of the SNS topic that delivers SES feedback. Its only role is source filtering: messages signed from any other topic are rejected. Set it whenever the token is set. |
| `EMAIL_OUTBOX_GLOBAL_DELIVERY_PAUSED` | No   | `false`              | Pauses all outbound email when true. |
| `EMAIL_OUTBOX_MAX_CONCURRENT_SENDS`    | No   | `10`                 | Concurrency bound for the outbox send workers. |
| `EMAIL_OUTBOX_MONTHLY_SEND_LIMIT`      | No   | `100000`             | Once monthly send attempts reach this number, delivery stops. |
| `EMAIL_OUTBOX_ESTIMATED_SEND_COST_MICROS` | No | `100`             | Estimated cost per send in micros, used for spend tracking. |
| `EMAIL_OUTBOX_PAUSED_WORKSPACE_IDS`    | No   | Empty                | Comma-separated workspace IDs whose mail stays queued. |

See [Email](/self-hosting/email) for transports and the SES webhook end to end.

## Media

The upload service resolves this config lazily per request, so a missing value fails the
first upload rather than server boot.

| Variable                             | Required | Default                              | Notes |
| ------------------------------------ | -------- | ------------------------------------ | ----- |
| `MEDIA_UPLOAD_REGION`                | Yes      | None (compose: `us-east-1`)          | S3 region. Use `auto` for Cloudflare R2. |
| `MEDIA_UPLOAD_ENDPOINT`              | Yes      | None (compose: `http://127.0.0.1:9002`) | S3-compatible endpoint. The compose default targets a local MinIO-style server; point it at your R2 or S3 endpoint. |
| `MEDIA_UPLOAD_ACCESS_KEY_ID`         | No       | None (compose: `feeblo`)             | S3 access key. |
| `MEDIA_UPLOAD_SECRET_ACCESS_KEY`     | No       | None (compose: `password`)           | S3 secret key. |
| `MEDIA_PUBLIC_BUCKET_NAME`           | Yes      | None (compose: `feeblo-media-public`) | Public bucket that uploaded media lands in. |

See [Media storage](/self-hosting/media) for S3, R2, and MinIO setups.

## Integrations

Each integration registers only when its full credential set is present; partial sets
disable the provider without any error. The settings below also back the outbound webhook
subsystem.

| Variable                              | Required | Default                | Notes |
| ------------------------------------- | -------- | ---------------------- | ----- |
| `INTEGRATION_ENCRYPTION_KEY`          | No       | Falls back to the auth at-rest key | Encrypts webhook endpoint URLs and signing secrets at rest. When it is shorter than 32 bytes, webhook create/update/rotate fails with `Webhook credentials could not be encrypted`. |
| `INTEGRATION_ALLOW_PRIVATE_NETWORK`   | No       | `false`                | Allows outbound webhook deliveries to private-network endpoints. The code honors it only when `NODE_ENV` is `development`; every other environment rejects private egress. Never enable it in production. |
| `SLACK_CLIENT_ID`                     | No       | None                   | Slack app credentials. The integration registers only when the client id, client secret, and signing secret are all set. |
| `SLACK_CLIENT_SECRET`                 | No       | None                   | |
| `SLACK_SIGNING_SECRET`                | No       | None                   | Request-signing secret from the Slack app's Basic Information page. |
| `SLACK_OAUTH_REDIRECT_URL`            | No       | The API URL plus `/slack/oauth/callback` | OAuth redirect for the Slack install flow. |
| `DISCORD_CLIENT_ID`                   | No       | None                   | Discord app credentials. The integration registers only when the client id, client secret, bot token, and public key are all set. |
| `DISCORD_CLIENT_SECRET`               | No       | None                   | |
| `DISCORD_BOT_TOKEN`                   | No       | None                   | Application-wide bot token; the same bot serves every guild install. |
| `DISCORD_PUBLIC_KEY`                  | No       | None                   | 64 hex characters; verifies interaction signatures. |
| `DISCORD_OAUTH_REDIRECT_URL`          | No       | The API URL plus `/discord/oauth/callback` | OAuth redirect for the Discord install flow. |
| `GITHUB_INTEGRATION_APP_ID`           | No       | None                   | Numeric App ID from the GitHub App's General page, not the OAuth client ID. The two-way issue integration registers only when all six values below are set. |
| `GITHUB_INTEGRATION_APP_SLUG`         | No       | None                   | URL-friendly app name from the public installation URL. For `https://github.com/apps/feeblo` the slug is `feeblo`. |
| `GITHUB_INTEGRATION_CLIENT_ID`        | No       | None                   | The install flow uses it only to verify the installer can reach the selected app installation. |
| `GITHUB_INTEGRATION_CLIENT_SECRET`    | No       | None                   | Paired with the client id above; the temporary user token is never stored. |
| `GITHUB_INTEGRATION_PRIVATE_KEY`      | No       | None                   | PEM private key from the app's Private keys section. Preserve the complete BEGIN/END lines and line breaks; store it as a multiline secret. |
| `GITHUB_INTEGRATION_WEBHOOK_SECRET`   | No       | None                   | Random secret configured identically on the GitHub App. Register the fixed callback `/github/app/installations/callback` and webhook `/github/app/webhooks` under the API base; Feeblo mounts both paths itself. |

The social-auth GitHub variables under Auth are separate: they only sign users into
Feeblo. The integration credentials group above powers the issue integration.

## Billing

| Variable                | Required | Default    | Notes |
| ----------------------- | -------- | ---------- | ----- |
| `POLAR_MODE`            | No       | `sandbox`  | Polar environment: `sandbox` or `production`. |
| `POLAR_ACCESS_TOKEN`    | No       | None       | Enables Polar checkout, the customer portal, and subscription synchronization. Without it the Polar client is never created and billing stays off. |
| `POLAR_WEBHOOK_SECRET`  | No       | None       | Verifies signed Polar webhooks. Point Polar at the webhook path `/api/auth/polar/webhooks` under the API base, then resend `product.created` and `product.updated` events so the local product catalog populates. |

## Observability & misc

| Variable                     | Required | Default                     | Notes |
| ---------------------------- | -------- | --------------------------- | ----- |
| `SENTRY_DSN`                 | No       | None                        | Turns on server error tracking, tracing, logs, and metrics. |
| `SENTRY_ENVIRONMENT`         | No       | `NODE_ENV`                  | Sentry environment label. Set it to `staging` while `NODE_ENV` stays `production`. |
| `SENTRY_TRACES_SAMPLE_RATE`  | No       | `0.1`                       | Fraction of transactions to trace (0.0–1.0). |
| `POSTHOG_KEY`                | No       | None                        | PostHog project API key for the dashboard. When omitted, all analytics is disabled. |
| `POSTHOG_HOST`               | No       | `https://us.i.posthog.com`  | PostHog instance host. |
| `EMBEDDING_API_KEY`          | No       | None                        | Enables AI post embeddings and semantic suggestions; the API must implement the OpenAI embeddings endpoint. An empty value keeps the feature off. |
| `EMBEDDING_MODEL`            | No       | `text-embedding-3-small`    | Embeddings model name. |
| `EMBEDDING_DIMENSIONS`       | No       | `1536`                      | Vector size. It must equal the post embedding column dimension (1536); reconfigure the column with `db:configure-embeddings` before changing it, or embedding jobs fail. |
| `EMBEDDING_API_URL`          | No       | None                        | Override for OpenAI-compatible providers such as Hugging Face or Ollama. |
| `PROFANITY_EXTRA_WORDS`      | No       | None                        | Comma-separated words appended to the bundled profanity dictionary. The server trims and lowercases each word. |

## Next steps

<CardGroup cols={2}>
  <Card title="Docker" href="/self-hosting/docker">
    The compose walkthrough and first boot.
  </Card>
  <Card title="Database" href="/self-hosting/database">
    Postgres specifics and the migration lifecycle.
  </Card>
  <Card title="Email" href="/self-hosting/email">
    SMTP transports, the SES webhook, and outbox controls.
  </Card>
  <Card title="Media" href="/self-hosting/media">
    S3, R2, and MinIO configuration.
  </Card>
  <Card title="OAuth sign-in" href="/self-hosting/oauth">
    GitHub and Google login setup.
  </Card>
  <Card title="Troubleshooting" href="/self-hosting/troubleshooting">
    Symptom to cause to fix.
  </Card>
</CardGroup>