From 1c1c95d332fb9ab6c2f926f6d51071cfb245ed86 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sun, 24 May 2026 12:55:41 +0200 Subject: [PATCH 1/6] docs(env): sync .env.self-host.example with missing variables Brings the self-hosting template into parity with apps/api/.env.example and the env vars wiki. Adds (mostly commented for visibility without changing default behaviour): - NODE_ENV, DATABASE_URL, DIRECT_DATABASE_URL, REDIS_URL, PORT (Docker auto-configures these via DB_PASSWORD; the lines document overrides) - Stripe billing block (STRIPE_SK, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_*, STRIPE_METER_EVENT_NAME) - Attachments (MAX_ATTACHMENT_SIZE_MB, MAX_ATTACHMENTS_COUNT) - SMTP_ENABLED in the SMTP Server block - VERIFY_EMAIL_ON_SIGNUP in User Management - Phishing Detection block (OPENROUTER_*, PHISHING_*) Each was either present in apps/api/.env.example or documented in the wiki but missing from the production template, forcing self-hosters to read source or the wiki to discover them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.self-host.example | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.env.self-host.example b/.env.self-host.example index 3da81745..eb72c4a9 100644 --- a/.env.self-host.example +++ b/.env.self-host.example @@ -8,6 +8,15 @@ DB_PASSWORD=changeme123 JWT_SECRET= +# The variables below are auto-configured by Docker Compose using DB_PASSWORD +# above. Override only when pointing at an external database / Redis or when +# running the app outside of the bundled Compose stack. +# NODE_ENV=production +# DATABASE_URL=postgresql://plunk:${DB_PASSWORD}@postgres:5432/plunk +# DIRECT_DATABASE_URL=postgresql://plunk:${DB_PASSWORD}@postgres:5432/plunk +# REDIS_URL=redis://redis:6379 +# PORT=8080 + # ======================================== # REQUIRED: Domains # ======================================== @@ -67,6 +76,16 @@ GITHUB_OAUTH_SECRET= GOOGLE_OAUTH_CLIENT= GOOGLE_OAUTH_SECRET= +# ======================================== +# OPTIONAL: Stripe (Billing) +# ======================================== +# Enables paid billing. All variables must be set together for billing to activate. +STRIPE_SK= +STRIPE_WEBHOOK_SECRET= +STRIPE_PRICE_ONBOARDING= +STRIPE_PRICE_EMAIL_USAGE= +STRIPE_METER_EVENT_NAME=emails + # ======================================== # OPTIONAL: File Storage (Minio) # ======================================== @@ -87,6 +106,14 @@ S3_BUCKET=uploads S3_PUBLIC_URL=http://localhost:9000/uploads S3_FORCE_PATH_STYLE=true +# ======================================== +# OPTIONAL: Attachments +# ======================================== +# Limits applied to attachments on transactional emails. +# AWS SES caps total message size at 40 MB; the defaults below leave headroom. +# MAX_ATTACHMENT_SIZE_MB=10 +# MAX_ATTACHMENTS_COUNT=10 + # ======================================== # OPTIONAL: Notifications (ntfy.sh) # ======================================== @@ -117,6 +144,11 @@ NTFY_URL=http://ntfy/plunk-notifications # Optional if using PEM files SMTP_DOMAIN=smtp.example.com +# Explicitly enable SMTP features in the UI. Automatically enabled when +# SMTP_DOMAIN is set to a non-localhost value in production. +# Default: false +# SMTP_ENABLED=false + # SMTP Ports (defaults work for most setups) # PORT_SECURE=465 # SMTPS (implicit TLS) # PORT_SUBMISSION=587 # SMTP Submission (STARTTLS) @@ -152,6 +184,25 @@ SMTP_DOMAIN=smtp.example.com # Default: false # DISABLE_SIGNUPS=false +# When enabled (true), validates emails on signup — checks for disposable +# domains, plus-addressing, domain existence, and MX records. +# Default: false +# VERIFY_EMAIL_ON_SIGNUP=false + +# ======================================== +# OPTIONAL: Phishing Detection +# ======================================== +# Plunk can use an LLM (via OpenRouter) to detect phishing emails before sending. +# When enabled, a random sample of outgoing emails is analyzed and projects can +# be auto-disabled if confidence exceeds the threshold or the cumulative count +# is reached within the window. +# OPENROUTER_API_KEY= +# OPENROUTER_MODEL=anthropic/claude-3-haiku +# PHISHING_DETECTION_SAMPLE_RATE=0.1 +# PHISHING_CONFIDENCE_THRESHOLD=95 +# PHISHING_CUMULATIVE_THRESHOLD=3 +# PHISHING_CUMULATIVE_WINDOW_MS=3600000 + # ======================================== # OPTIONAL: SES Sending Rate # ======================================== From 81315eac8eb32a7d2e61617c6a84f349afaf7860 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sun, 24 May 2026 12:55:52 +0200 Subject: [PATCH 2/6] docs(env): add wiki-documented vars to apps/api/.env.example Brings the dev template into parity with the env vars wiki for variables a developer might toggle locally: - PORT (in Environment & Security) - PLUNK_FROM_ADDRESS (in Plunk API) - New sections: Notifications (NTFY_URL), Attachments, User Management (DISABLE_SIGNUPS, VERIFY_EMAIL_ON_SIGNUP), Phishing Detection All additions are commented so default local behaviour is unchanged. Vars that only make sense in the bundled Docker stack (SMTP relay ports, Minio internals, NEXT_PUBLIC_*, NGINX_PORT) are intentionally omitted. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/.env.example | 47 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/apps/api/.env.example b/apps/api/.env.example index 2d8beb7f..eecaf180 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -8,6 +8,8 @@ # ============================================================================== NODE_ENV=development JWT_SECRET=hBx9Xh8J6KOMAGAsSjvcZJBT5TWyIkFX +# Port the API server listens on (default: 8080) +# PORT=8080 # ============================================================================== # Application URLs @@ -25,6 +27,8 @@ LANDING_URI=http://localhost:4000 # ============================================================================== # API key for authenticating with the Plunk API (obtained from dashboard) PLUNK_API_KEY= +# From address used for platform notification emails (project disabled, billing limits, etc.) +# PLUNK_FROM_ADDRESS= # ============================================================================== # Database & Redis @@ -85,3 +89,46 @@ STRIPE_METER_EVENT_NAME=emails # Meter event name (API key from your Stripe mete # Set to 'false' to disable automatic project suspension (useful for self-hosters who manage manually) # Default: true (automatic project disabling enabled) # AUTO_PROJECT_DISABLE=true + +# ============================================================================== +# Notifications (Optional - system notifications via ntfy) +# ============================================================================== +# ntfy topic URL for internal system notifications (e.g. project disabled, billing limits). +# When unset, ntfy notifications are disabled. +# Examples: +# - Public ntfy.sh: https://ntfy.sh/your-unique-topic-name +# - Self-hosted: https://your-ntfy-server.com/your-topic +# NTFY_URL= + +# ============================================================================== +# Attachments (Optional) +# ============================================================================== +# Limits applied to attachments on transactional emails. +# AWS SES caps total message size at 40 MB; the defaults below leave headroom. +# MAX_ATTACHMENT_SIZE_MB=10 +# MAX_ATTACHMENTS_COUNT=10 + +# ============================================================================== +# User Management (Optional) +# ============================================================================== +# When 'true', the signup endpoint rejects new user registrations. +# Default: false +# DISABLE_SIGNUPS=false +# When 'true', validates emails on signup (disposable domains, plus-addressing, +# domain existence, MX records). +# Default: false +# VERIFY_EMAIL_ON_SIGNUP=false + +# ============================================================================== +# Phishing Detection (Optional - AI-powered phishing scan via OpenRouter) +# ============================================================================== +# When OPENROUTER_API_KEY is set, a random sample of outgoing emails is +# analyzed by an LLM. Projects can be auto-disabled if a single email exceeds +# PHISHING_CONFIDENCE_THRESHOLD, or if PHISHING_CUMULATIVE_THRESHOLD emails are +# flagged within PHISHING_CUMULATIVE_WINDOW_MS. +# OPENROUTER_API_KEY= +# OPENROUTER_MODEL=anthropic/claude-3-haiku +# PHISHING_DETECTION_SAMPLE_RATE=0.1 +# PHISHING_CONFIDENCE_THRESHOLD=95 +# PHISHING_CUMULATIVE_THRESHOLD=3 +# PHISHING_CUMULATIVE_WINDOW_MS=3600000 From 971b98a4cc6145571204f5a4ad794aa3d09ea71c Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sun, 24 May 2026 12:56:00 +0200 Subject: [PATCH 3/6] docs(wiki): document MAIL_FROM_SUBDOMAIN and NGINX_PORT env vars Both variables exist in .env.self-host.example but were missing from the user-facing env vars reference, so self-hosters had to read the example file's inline comments to discover them. - MAIL_FROM_SUBDOMAIN: added a row to the AWS SES section explaining how the prefix combines with a verified domain to construct the MAIL FROM hostname, and when to override the default. - NGINX_PORT: added a new "Advanced" section for variables that almost never need tuning, with the host-port override use-case spelled out. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../content/docs/self-hosting/environment-variables.mdx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/wiki/content/docs/self-hosting/environment-variables.mdx b/apps/wiki/content/docs/self-hosting/environment-variables.mdx index 8afe7b02..eeccc95b 100644 --- a/apps/wiki/content/docs/self-hosting/environment-variables.mdx +++ b/apps/wiki/content/docs/self-hosting/environment-variables.mdx @@ -36,6 +36,7 @@ Set your subdomains here. The application automatically derives all internal and | `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) @@ -130,6 +131,14 @@ Plunk bundles a self-hosted [ntfy](https://ntfy.sh) server for internal system n | `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. | — | +## 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. From 5c166797b57f2e7b714957824ee1acdcb22c05f2 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sun, 24 May 2026 12:56:15 +0200 Subject: [PATCH 4/6] docs: correct PHISHING_CONFIDENCE_THRESHOLD default in CLAUDE.md CLAUDE.md listed the default as 85, but the source of truth (apps/api/src/app/constants.ts:137) and the env vars wiki both use 95. Aligning CLAUDE.md so future contributors don't propagate the stale value into new code or docs. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 44acce3a..ecfac46a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,7 +161,7 @@ Required for builds and deployment (see turbo.json and .env.example): - `OPENROUTER_API_KEY` - API key for OpenRouter (enables phishing detection) - `OPENROUTER_MODEL` (default: anthropic/claude-3-haiku) - LLM model to use for content analysis - `PHISHING_DETECTION_SAMPLE_RATE` (default: 0.1) - Percentage of emails to check (0.0-1.0, e.g., 0.1 = 10%) - - `PHISHING_CONFIDENCE_THRESHOLD` (default: 85) - Minimum confidence percentage (0-100) to auto-disable project for single detection + - `PHISHING_CONFIDENCE_THRESHOLD` (default: 95) - Minimum confidence percentage (0-100) to auto-disable project for single detection - `PHISHING_CUMULATIVE_THRESHOLD` (default: 3) - Number of phishing detections within time window to trigger auto-disable - `PHISHING_CUMULATIVE_WINDOW_MS` (default: 3600000) - Time window in milliseconds for cumulative tracking (default 1 hour) From a348d37c21ecf1d57845f31210f66a8113fadb8d Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sun, 24 May 2026 12:56:23 +0200 Subject: [PATCH 5/6] docs: add env-var sync rule to CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plunk has three sources of truth for environment variables: 1. apps/api/.env.example — local development defaults 2. .env.self-host.example — self-hosting/production template 3. apps/wiki/content/docs/self-hosting/environment-variables.mdx — user-facing reference They have drifted repeatedly when new vars land in one location only. Codify the requirement to update all three in the same change as part of CLAUDE.md so future Claude-assisted (and human) contributions don't reintroduce the drift. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index ecfac46a..deb33eb2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,6 +174,21 @@ Required for builds and deployment (see turbo.json and .env.example): - **Frontend Variables**: Next.js apps use `NEXT_PUBLIC_*` prefixed variables that are embedded at build time for client-side access +## Environment Variable Changes + +When you add, rename, remove, or change the default/behaviour of any environment variable, you MUST update all THREE of the following in the same change: + +1. `apps/api/.env.example` — local development defaults +2. `.env.self-host.example` — self-hosting / production template +3. `apps/wiki/content/docs/self-hosting/environment-variables.mdx` — user-facing reference + +Rules: + +- If the variable already exists in any file, **modify** its line/row/description — do not duplicate or leave a stale entry. +- Keep section/category names consistent across all three files (e.g. "AWS SES", "Phishing Detection"). +- For dev-only or self-host-only variables, still mention them in the wiki and note the scope; only skip the example file where the variable is genuinely never applicable. +- When in doubt about whether a variable belongs in `apps/api/.env.example` (development), include it commented out with a short note. + ## Plugins There are two plugins installed for you to use. From 87eb56ff36d56c387125b4efcfe3088d9aa9ccbf Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Sun, 24 May 2026 16:05:25 +0200 Subject: [PATCH 6/6] Revert "docs(env): sync .env.self-host.example with missing variables" This reverts commit 1c1c95d332fb9ab6c2f926f6d51071cfb245ed86. --- .env.self-host.example | 51 ------------------------------------------ 1 file changed, 51 deletions(-) diff --git a/.env.self-host.example b/.env.self-host.example index eb72c4a9..3da81745 100644 --- a/.env.self-host.example +++ b/.env.self-host.example @@ -8,15 +8,6 @@ DB_PASSWORD=changeme123 JWT_SECRET= -# The variables below are auto-configured by Docker Compose using DB_PASSWORD -# above. Override only when pointing at an external database / Redis or when -# running the app outside of the bundled Compose stack. -# NODE_ENV=production -# DATABASE_URL=postgresql://plunk:${DB_PASSWORD}@postgres:5432/plunk -# DIRECT_DATABASE_URL=postgresql://plunk:${DB_PASSWORD}@postgres:5432/plunk -# REDIS_URL=redis://redis:6379 -# PORT=8080 - # ======================================== # REQUIRED: Domains # ======================================== @@ -76,16 +67,6 @@ GITHUB_OAUTH_SECRET= GOOGLE_OAUTH_CLIENT= GOOGLE_OAUTH_SECRET= -# ======================================== -# OPTIONAL: Stripe (Billing) -# ======================================== -# Enables paid billing. All variables must be set together for billing to activate. -STRIPE_SK= -STRIPE_WEBHOOK_SECRET= -STRIPE_PRICE_ONBOARDING= -STRIPE_PRICE_EMAIL_USAGE= -STRIPE_METER_EVENT_NAME=emails - # ======================================== # OPTIONAL: File Storage (Minio) # ======================================== @@ -106,14 +87,6 @@ S3_BUCKET=uploads S3_PUBLIC_URL=http://localhost:9000/uploads S3_FORCE_PATH_STYLE=true -# ======================================== -# OPTIONAL: Attachments -# ======================================== -# Limits applied to attachments on transactional emails. -# AWS SES caps total message size at 40 MB; the defaults below leave headroom. -# MAX_ATTACHMENT_SIZE_MB=10 -# MAX_ATTACHMENTS_COUNT=10 - # ======================================== # OPTIONAL: Notifications (ntfy.sh) # ======================================== @@ -144,11 +117,6 @@ NTFY_URL=http://ntfy/plunk-notifications # Optional if using PEM files SMTP_DOMAIN=smtp.example.com -# Explicitly enable SMTP features in the UI. Automatically enabled when -# SMTP_DOMAIN is set to a non-localhost value in production. -# Default: false -# SMTP_ENABLED=false - # SMTP Ports (defaults work for most setups) # PORT_SECURE=465 # SMTPS (implicit TLS) # PORT_SUBMISSION=587 # SMTP Submission (STARTTLS) @@ -184,25 +152,6 @@ SMTP_DOMAIN=smtp.example.com # Default: false # DISABLE_SIGNUPS=false -# When enabled (true), validates emails on signup — checks for disposable -# domains, plus-addressing, domain existence, and MX records. -# Default: false -# VERIFY_EMAIL_ON_SIGNUP=false - -# ======================================== -# OPTIONAL: Phishing Detection -# ======================================== -# Plunk can use an LLM (via OpenRouter) to detect phishing emails before sending. -# When enabled, a random sample of outgoing emails is analyzed and projects can -# be auto-disabled if confidence exceeds the threshold or the cumulative count -# is reached within the window. -# OPENROUTER_API_KEY= -# OPENROUTER_MODEL=anthropic/claude-3-haiku -# PHISHING_DETECTION_SAMPLE_RATE=0.1 -# PHISHING_CONFIDENCE_THRESHOLD=95 -# PHISHING_CUMULATIVE_THRESHOLD=3 -# PHISHING_CUMULATIVE_WINDOW_MS=3600000 - # ======================================== # OPTIONAL: SES Sending Rate # ========================================