# Environment Variables (/self-hosting/environment-variables)

## Security & Database

| Variable              | Required | Description                                                                                                                    | Example                                           |
| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| `JWT_SECRET`          | Yes      | Secret key used to sign JWT tokens. Generate with `openssl rand -base64 32`.                                                   | `s3cr3t...`                                       |
| `DB_PASSWORD`         | Yes      | PostgreSQL database password. Used by the Docker Compose setup.                                                                | `changeme123`                                     |
| `DATABASE_URL`        | Yes      | Full PostgreSQL connection string used by the application at runtime (typically through PgBouncer). Auto-configured in Docker. | `postgresql://plunk:password@postgres:5432/plunk` |
| `DIRECT_DATABASE_URL` | Yes      | Direct PostgreSQL connection string used by Prisma migrations. Must bypass any connection pooler. Auto-configured in Docker.   | `postgresql://plunk:password@postgres:5432/plunk` |
| `REDIS_URL`           | Yes      | Redis connection string.                                                                                                       | `redis://redis:6379`                              |
| `NODE_ENV`            | No       | Application environment. Set to `production` for production deployments.                                                       | `production`                                      |
| `PORT`                | No       | Port the API server listens on.                                                                                                | `8080` (default)                                  |

## URLs & Domains

Set your subdomains here. The application automatically derives all internal and client-side URLs from these at container startup — you don't need to set `*_URI` or `NEXT_PUBLIC_*` variables manually.

| Variable           | Required | Description                                                                                                          | Example               |
| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `API_DOMAIN`       | Yes      | Subdomain for the API server.                                                                                        | `api.yourdomain.com`  |
| `DASHBOARD_DOMAIN` | Yes      | Subdomain for the dashboard app.                                                                                     | `app.yourdomain.com`  |
| `LANDING_DOMAIN`   | Yes      | Subdomain for the landing page.                                                                                      | `www.yourdomain.com`  |
| `WIKI_DOMAIN`      | Yes      | Subdomain for the documentation site.                                                                                | `docs.yourdomain.com` |
| `USE_HTTPS`        | No       | Set to `true` when running behind a TLS-terminating reverse proxy. Used to construct URLs with the correct protocol. | `false` (default)     |

## AWS SES

| Variable                            | Required | Description                                                                                                                                                                                                                                                                                                                                                          | Example                                         |
| ----------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `AWS_SES_REGION`                    | Yes      | AWS region where SES is configured.                                                                                                                                                                                                                                                                                                                                  | `us-east-1`                                     |
| `AWS_SES_ACCESS_KEY_ID`             | Yes      | AWS access key ID with SES send permissions.                                                                                                                                                                                                                                                                                                                         | `AKIA...`                                       |
| `AWS_SES_SECRET_ACCESS_KEY`         | Yes      | AWS secret access key for SES.                                                                                                                                                                                                                                                                                                                                       | `wJalr...`                                      |
| `SES_CONFIGURATION_SET`             | No       | SES configuration set name used for open/click tracking.                                                                                                                                                                                                                                                                                                             | `plunk-configuration-set` (default)             |
| `SES_CONFIGURATION_SET_NO_TRACKING` | No       | A second SES configuration set without tracking. When set, projects can toggle email tracking on/off. If omitted, the tracking toggle is hidden.                                                                                                                                                                                                                     | `plunk-no-tracking-configuration-set` (default) |
| `MAIL_FROM_SUBDOMAIN`               | No       | Subdomain prefix used when constructing the MAIL FROM hostname for a verified domain (e.g. with default `plunk` and domain `yourdomain.com`, the MAIL FROM is `plunk.yourdomain.com`). Override when the default subdomain is already in use (e.g. by an R2/CDN custom domain), since the MAIL FROM hostname needs MX + TXT records that can't coexist with a CNAME. | `plunk`                                         |

## Storage (Minio)

The bundled Docker setup includes Minio with defaults that work out of the box. Only change these when connecting to an external S3-compatible bucket.

| Variable               | Required | Description                                                        | Default             |
| ---------------------- | -------- | ------------------------------------------------------------------ | ------------------- |
| `MINIO_ROOT_USER`      | No       | Minio root username (Docker Compose only).                         | `plunk`             |
| `MINIO_ROOT_PASSWORD`  | No       | Minio root password (Docker Compose only).                         | `plunkminiopass`    |
| `MINIO_API_PORT`       | No       | Port for the Minio API (Docker Compose only).                      | `9000`              |
| `MINIO_CONSOLE_PORT`   | No       | Port for the Minio console UI (Docker Compose only).               | `9001`              |
| `S3_ENDPOINT`          | No       | S3 or Minio endpoint URL.                                          | `http://minio:9000` |
| `S3_ACCESS_KEY_ID`     | No       | S3 or Minio access key.                                            | —                   |
| `S3_ACCESS_KEY_SECRET` | No       | S3 or Minio secret key.                                            | —                   |
| `S3_BUCKET`            | No       | Bucket name for file uploads.                                      | `uploads`           |
| `S3_PUBLIC_URL`        | No       | Publicly accessible base URL for stored files.                     | —                   |
| `S3_FORCE_PATH_STYLE`  | No       | Use path-style URLs instead of virtual-hosted. Required for Minio. | `true`              |

## Attachments

Plunk supports attachments on transactional emails. AWS SES caps total message size at 40 MB; the defaults below leave headroom but can be tuned for your use case.

| Variable                 | Required | Description                                                                                          | Default |
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------- | ------- |
| `MAX_ATTACHMENT_SIZE_MB` | No       | Maximum total attachment size in megabytes per email. Hard upper bound enforced by AWS SES is 40 MB. | `10`    |
| `MAX_ATTACHMENTS_COUNT`  | No       | Maximum number of attachments per email.                                                             | `10`    |

## API Rate Limiting

The high-traffic API endpoints are protected by per-project token buckets stored in Redis. Each bucket has a sustained rate (`*_PER_SECOND`) and a burst ceiling (`*_BURST`) that absorbs spikes — useful for callers like real-time contact sync, which drains a backlog all at once and then idles.

Limits are keyed on the project rather than the IP address, so one customer's runaway integration cannot consume another's budget. Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a throttled request returns `429` with a `Retry-After` header. If Redis is unreachable, requests are allowed through rather than rejected.

Rate limiting is **off by default** — upgrading Plunk will never start refusing traffic your callers were already sending. Set `RATE_LIMIT_ENABLED=true` to switch it on; none of the other variables have any effect until you do. Once enabled, set an individual `*_PER_SECOND` to `0` to disable a single bucket while leaving the others active.

| Variable                         | Required | Description                                                                                     | Default |
| -------------------------------- | -------- | ----------------------------------------------------------------------------------------------- | ------- |
| `RATE_LIMIT_ENABLED`             | No       | Master switch for all API rate limiting. Turn on when exposed to untrusted API clients.         | `false` |
| `RATE_LIMIT_TRACK_PER_SECOND`    | No       | Sustained requests per second for `POST /v1/track`, usually the highest-volume endpoint.        | `100`   |
| `RATE_LIMIT_TRACK_BURST`         | No       | Burst ceiling for `POST /v1/track`.                                                             | `200`   |
| `RATE_LIMIT_SEND_PER_SECOND`     | No       | Sustained requests per second for `POST /v1/send`. Also bounded by the project's billing limit. | `20`    |
| `RATE_LIMIT_SEND_BURST`          | No       | Burst ceiling for `POST /v1/send`.                                                              | `50`    |
| `RATE_LIMIT_CONTACTS_PER_SECOND` | No       | Sustained requests per second across contact writes (`POST`, `PATCH`, `DELETE` on `/contacts`). | `50`    |
| `RATE_LIMIT_CONTACTS_BURST`      | No       | Burst ceiling shared by those contact writes.                                                   | `100`   |

Contact writes share one budget because a sync integration drives create, update and delete from the same upstream change feed — separate budgets would let a single runaway sync spend three times the intended allowance.

## Idempotency

Callers can pass an `Idempotency-Key` header to `POST /v1/track` and `POST /v1/send` to guarantee the request runs at most once. See [Idempotency](/guides/idempotency).

| Variable                    | Required | Description                                                                                                                                                  | Default |
| --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `IDEMPOTENCY_KEY_TTL_HOURS` | No       | How long a used key stays claimed before it can be reused. Expired keys are swept hourly, so raising this grows the `idempotency_keys` table proportionally. | `24`    |

## SMTP Server

The optional SMTP relay lets you send emails through Plunk via the SMTP protocol.

| Variable          | Required | Description                                                                                                                        | Default     |
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `SMTP_DOMAIN`     | No       | SMTP relay domain. Required when using Traefik's `acme.json` with multiple certificates so the correct cert can be selected.       | `localhost` |
| `SMTP_ENABLED`    | No       | Explicitly enable SMTP features in the UI. Automatically enabled when `SMTP_DOMAIN` is set to a non-localhost value in production. | `false`     |
| `PORT_SECURE`     | No       | SMTPS port (implicit TLS).                                                                                                         | `465`       |
| `PORT_SUBMISSION` | No       | SMTP submission port (STARTTLS).                                                                                                   | `587`       |
| `MAX_RECIPIENTS`  | No       | Maximum number of recipients per email.                                                                                            | `5`         |

## OAuth

Enables social login. Register an OAuth app with each provider and add the credentials here.

| Variable              | Required | Description                     |
| --------------------- | -------- | ------------------------------- |
| `GITHUB_OAUTH_CLIENT` | No       | GitHub OAuth app client ID.     |
| `GITHUB_OAUTH_SECRET` | No       | GitHub OAuth app client secret. |
| `GOOGLE_OAUTH_CLIENT` | No       | Google OAuth app client ID.     |
| `GOOGLE_OAUTH_SECRET` | No       | Google OAuth app client secret. |

## Stripe

Required if you want to enable billing features. All Stripe variables must be set together for billing to activate.

\| Variable                   | Required | Description                                         |
\| -------------------------- | -------- | --------------------------------------------------- | ------------------ |
\| `STRIPE_SK`                | No       | Stripe secret key.                                  |
\| `STRIPE_WEBHOOK_SECRET`    | No       | Stripe webhook signing secret for verifying events. |
\| `STRIPE_PRICE_EMAIL_USAGE` | No       | Stripe price ID for metered pay-per-email usage.    |
\| `STRIPE_METER_EVENT_NAME`  | No       | Stripe meter event name.                            | `emails` (default) |

Checkout collects nothing upfront. Right after it completes, Plunk places a single charge of 2.00 (in the customer's billing currency) to confirm the card accepts off-session, merchant-initiated payments. This charge is created directly rather than from a price ID, so there's no variable for it. It's credited back to the customer's Stripe balance once verification succeeds. The same check runs whenever the customer changes their default card, which requires the `customer.updated` and `customer.subscription.updated` events on your Stripe webhook endpoint. See [Billing](/concepts/billing) for the customer-facing description.

<Callout type="info">
  `STRIPE_PRICE_ONBOARDING` has been removed. The separate one-time onboarding fee no longer exists, so you can delete the variable from your environment; it's ignored if still set.
</Callout>

<Callout type="warn">
  If you use a Stripe Radar rule to request 3D Secure, exclude off-session payments from it
  (`and not :is_off_session:`). The verification charge runs with no cardholder present, so a 3DS request it
  cannot satisfy will leave every verification unresolved.
</Callout>

## Platform Emails

When configured, Plunk will send email notifications to users for critical events (e.g. project disabled, billing limits reached). Without these, only ntfy notifications are sent.

| Variable             | Required | Description                                                | Example                  |
| -------------------- | -------- | ---------------------------------------------------------- | ------------------------ |
| `PLUNK_API_KEY`      | No       | API key for a Plunk instance to send transactional emails. | `pk_...`                 |
| `PLUNK_FROM_ADDRESS` | No       | From address used for platform notification emails.        | `noreply@yourdomain.com` |

## Notifications (ntfy)

Plunk bundles a self-hosted [ntfy](https://ntfy.sh) server for internal system notifications.

| Variable    | Required | Description                                                                         | Default                           |
| ----------- | -------- | ----------------------------------------------------------------------------------- | --------------------------------- |
| `NTFY_PORT` | No       | Port for the ntfy web UI (Docker Compose only).                                     | `8080`                            |
| `NTFY_URL`  | No       | ntfy topic URL. Change this to use an external ntfy.sh server or your own instance. | `http://ntfy/plunk-notifications` |

## User Management

| Variable                 | Required | Description                                                                                                                 | Default |
| ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | ------- |
| `DISABLE_SIGNUPS`        | No       | When `true`, the signup endpoint rejects new registrations. Useful for private instances.                                   | `false` |
| `VERIFY_EMAIL_ON_SIGNUP` | No       | When `true`, validates emails on signup — checks for disposable domains, plus-addressing, domain existence, and MX records. | `false` |

## Interface

| Variable       | Required | Description                                                                                                                                         | Default |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `SHOW_SPONSOR` | No       | When `true`, the dashboard navigation shows a **Sponsor** link to [GitHub Sponsors](https://github.com/sponsors/driaug). Set to `false` to hide it. | `true`  |

## Security

| Variable                       | Required | Description                                                                                                                                                 | Default |
| ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `AUTO_PROJECT_DISABLE`         | No       | When `true`, projects are automatically suspended when bounce or complaint rate thresholds are exceeded. Set to `false` to manage project status manually.  | `true`  |
| `EMAIL_RATE_LIMIT_PER_SECOND`  | No       | Override the email sending rate limit. If not set, Plunk automatically fetches the quota from your AWS SES account.                                         | —       |
| `EMAIL_WORKER_CONCURRENCY`     | No       | Number of emails the worker processes in parallel. When unset, derived from the effective rate limit so a higher SES quota scales throughput automatically. | —       |
| `EMAIL_WORKER_MAX_CONCURRENCY` | No       | Upper bound applied to the auto-derived worker concurrency. Raise this only after sizing the Prisma connection pool accordingly.                            | `50`    |

## Advanced

Variables for unusual deployments. The defaults work for the standard Docker Compose setup — only change these if you know you need to.

| Variable     | Required | Description                                                                                                                                                                                | Default |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `NGINX_PORT` | No       | Host port the bundled Nginx reverse proxy binds to. Override when port 80/443 is already in use on the host (e.g. running behind another reverse proxy that forwards to a different port). | `80`    |

## Phishing Detection

Plunk can use AI to detect and block phishing emails before they're sent. Requires an [OpenRouter](https://openrouter.ai) API key.

| Variable                         | Required | Description                                                                                       | Default                    |
| -------------------------------- | -------- | ------------------------------------------------------------------------------------------------- | -------------------------- |
| `OPENROUTER_API_KEY`             | No       | OpenRouter API key. When set, enables AI-powered phishing detection.                              | —                          |
| `OPENROUTER_MODEL`               | No       | LLM model to use for content analysis. See [OpenRouter models](https://openrouter.ai/models).     | `anthropic/claude-3-haiku` |
| `PHISHING_DETECTION_SAMPLE_RATE` | No       | Percentage of emails to check (0.0-1.0). For example, `0.1` means 10% of emails are analyzed.     | `0.1` (10%)                |
| `PHISHING_CONFIDENCE_THRESHOLD`  | No       | Minimum confidence percentage (0-100) required to auto-disable a project from a single detection. | `95`                       |
| `PHISHING_CUMULATIVE_THRESHOLD`  | No       | Number of phishing detections within the time window required to auto-disable a project.          | `3`                        |
| `PHISHING_CUMULATIVE_WINDOW_MS`  | No       | Time window in milliseconds for cumulative phishing tracking.                                     | `3600000` (1 hour)         |

**How it works:**

* A random sample of emails (controlled by `PHISHING_DETECTION_SAMPLE_RATE`) are analyzed by the LLM for phishing content.
* Projects are automatically disabled if **either**:
  1. A single email is detected with confidence ≥ `PHISHING_CONFIDENCE_THRESHOLD`, **or**
  2. `PHISHING_CUMULATIVE_THRESHOLD` or more emails are flagged within the `PHISHING_CUMULATIVE_WINDOW_MS` time window.
* Detection history is stored in Redis and shared across all worker instances.
