PlunkPlunk
Self-Hosting

Environment Variables

Configuration reference

Security & Database

VariableRequiredDescriptionExample
JWT_SECRETYesSecret key used to sign JWT tokens. Generate with openssl rand -base64 32.s3cr3t...
DB_PASSWORDYesPostgreSQL database password. Used by the Docker Compose setup.changeme123
DATABASE_URLYesFull PostgreSQL connection string used by the application at runtime (typically through PgBouncer). Auto-configured in Docker.postgresql://plunk:password@postgres:5432/plunk
DIRECT_DATABASE_URLYesDirect PostgreSQL connection string used by Prisma migrations. Must bypass any connection pooler. Auto-configured in Docker.postgresql://plunk:password@postgres:5432/plunk
REDIS_URLYesRedis connection string.redis://redis:6379
NODE_ENVNoApplication environment. Set to production for production deployments.production
PORTNoPort 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.

VariableRequiredDescriptionExample
API_DOMAINYesSubdomain for the API server.api.yourdomain.com
DASHBOARD_DOMAINYesSubdomain for the dashboard app.app.yourdomain.com
LANDING_DOMAINYesSubdomain for the landing page.www.yourdomain.com
WIKI_DOMAINYesSubdomain for the documentation site.docs.yourdomain.com
USE_HTTPSNoSet to true when running behind a TLS-terminating reverse proxy. Used to construct URLs with the correct protocol.false (default)

AWS SES

VariableRequiredDescriptionExample
AWS_SES_REGIONYesAWS region where SES is configured.us-east-1
AWS_SES_ACCESS_KEY_IDYesAWS access key ID with SES send permissions.AKIA...
AWS_SES_SECRET_ACCESS_KEYYesAWS secret access key for SES.wJalr...
SES_CONFIGURATION_SETNoSES configuration set name used for open/click tracking.plunk-configuration-set (default)
SES_CONFIGURATION_SET_NO_TRACKINGNoA 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_SUBDOMAINNoSubdomain 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.

VariableRequiredDescriptionDefault
MINIO_ROOT_USERNoMinio root username (Docker Compose only).plunk
MINIO_ROOT_PASSWORDNoMinio root password (Docker Compose only).plunkminiopass
MINIO_API_PORTNoPort for the Minio API (Docker Compose only).9000
MINIO_CONSOLE_PORTNoPort for the Minio console UI (Docker Compose only).9001
S3_ENDPOINTNoS3 or Minio endpoint URL.http://minio:9000
S3_ACCESS_KEY_IDNoS3 or Minio access key.
S3_ACCESS_KEY_SECRETNoS3 or Minio secret key.
S3_BUCKETNoBucket name for file uploads.uploads
S3_PUBLIC_URLNoPublicly accessible base URL for stored files.
S3_FORCE_PATH_STYLENoUse 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.

VariableRequiredDescriptionDefault
MAX_ATTACHMENT_SIZE_MBNoMaximum total attachment size in megabytes per email. Hard upper bound enforced by AWS SES is 40 MB.10
MAX_ATTACHMENTS_COUNTNoMaximum 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.

VariableRequiredDescriptionDefault
RATE_LIMIT_ENABLEDNoMaster switch for all API rate limiting. Turn on when exposed to untrusted API clients.false
RATE_LIMIT_TRACK_PER_SECONDNoSustained requests per second for POST /v1/track, usually the highest-volume endpoint.100
RATE_LIMIT_TRACK_BURSTNoBurst ceiling for POST /v1/track.200
RATE_LIMIT_SEND_PER_SECONDNoSustained requests per second for POST /v1/send. Also bounded by the project's billing limit.20
RATE_LIMIT_SEND_BURSTNoBurst ceiling for POST /v1/send.50
RATE_LIMIT_CONTACTS_PER_SECONDNoSustained requests per second across contact writes (POST, PATCH, DELETE on /contacts).50
RATE_LIMIT_CONTACTS_BURSTNoBurst 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.

VariableRequiredDescriptionDefault
IDEMPOTENCY_KEY_TTL_HOURSNoHow 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.

VariableRequiredDescriptionDefault
SMTP_DOMAINNoSMTP relay domain. Required when using Traefik's acme.json with multiple certificates so the correct cert can be selected.localhost
SMTP_ENABLEDNoExplicitly enable SMTP features in the UI. Automatically enabled when SMTP_DOMAIN is set to a non-localhost value in production.false
PORT_SECURENoSMTPS port (implicit TLS).465
PORT_SUBMISSIONNoSMTP submission port (STARTTLS).587
MAX_RECIPIENTSNoMaximum number of recipients per email.5

OAuth

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

VariableRequiredDescription
GITHUB_OAUTH_CLIENTNoGitHub OAuth app client ID.
GITHUB_OAUTH_SECRETNoGitHub OAuth app client secret.
GOOGLE_OAUTH_CLIENTNoGoogle OAuth app client ID.
GOOGLE_OAUTH_SECRETNoGoogle 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_ONBOARDING | No | Stripe price ID for the one-time onboarding fee. | | 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) |

On top of the onboarding fee, activating a subscription places a second charge of 1.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. Both charges are credited back to the customer's Stripe balance once verification succeeds. See Billing for the customer-facing description.

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.

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.

VariableRequiredDescriptionExample
PLUNK_API_KEYNoAPI key for a Plunk instance to send transactional emails.pk_...
PLUNK_FROM_ADDRESSNoFrom address used for platform notification emails.noreply@yourdomain.com

Notifications (ntfy)

Plunk bundles a self-hosted ntfy server for internal system notifications.

VariableRequiredDescriptionDefault
NTFY_PORTNoPort for the ntfy web UI (Docker Compose only).8080
NTFY_URLNontfy topic URL. Change this to use an external ntfy.sh server or your own instance.http://ntfy/plunk-notifications

User Management

VariableRequiredDescriptionDefault
DISABLE_SIGNUPSNoWhen true, the signup endpoint rejects new registrations. Useful for private instances.false
VERIFY_EMAIL_ON_SIGNUPNoWhen true, validates emails on signup — checks for disposable domains, plus-addressing, domain existence, and MX records.false

Security

VariableRequiredDescriptionDefault
AUTO_PROJECT_DISABLENoWhen 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_SECONDNoOverride the email sending rate limit. If not set, Plunk automatically fetches the quota from your AWS SES account.
EMAIL_WORKER_CONCURRENCYNoNumber 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_CONCURRENCYNoUpper 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.

VariableRequiredDescriptionDefault
NGINX_PORTNoHost 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 API key.

VariableRequiredDescriptionDefault
OPENROUTER_API_KEYNoOpenRouter API key. When set, enables AI-powered phishing detection.
OPENROUTER_MODELNoLLM model to use for content analysis. See OpenRouter models.anthropic/claude-3-haiku
PHISHING_DETECTION_SAMPLE_RATENoPercentage of emails to check (0.0-1.0). For example, 0.1 means 10% of emails are analyzed.0.1 (10%)
PHISHING_CONFIDENCE_THRESHOLDNoMinimum confidence percentage (0-100) required to auto-disable a project from a single detection.95
PHISHING_CUMULATIVE_THRESHOLDNoNumber of phishing detections within the time window required to auto-disable a project.3
PHISHING_CUMULATIVE_WINDOW_MSNoTime 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.