SendlySendly
Self-Hosting

Environment Variables

Configuration reference

Security & Database

VariableRequiredDescriptionExample
BETTER_AUTH_SECRETYesSigning/encryption secret for sessions and for the secret-reveal key (HKDF-derived from it). Must be at least 32 characters. Generate with openssl rand -base64 32.s3cr3t...
DB_PASSWORDYesPostgreSQL database password. Used by the Docker Compose setup.changeme123
DATABASE_URLYesFull PostgreSQL connection string. Auto-configured in Docker.postgresql://sendly:password@postgres:5432/sendly
REDIS_URLYesRedis connection string.redis://redis:6379
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.sendly-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.sendly-no-tracking-configuration-set (default)

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).sendly
MINIO_ROOT_PASSWORDNoMinio root password (Docker Compose only).sendlyminiopass
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

SMTP Server

The optional SMTP relay lets you send emails through Sendly 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

All four are required, and there is no "billing disabled" mode. While they were optional the app derived a STRIPE_ENABLED flag from their presence, which made the monthly usage cap, the pre-send campaign billing check and usage metering into silent no-ops on any install that had not configured Stripe — enforcement became a function of how completely someone had filled in their environment. A missing value now fails boot instead.

There is no flat-monthly price. Checkout sells one metered line item, and STRIPE_PRICE_PRO_FLAT_MONTHLY (formerly STRIPE_PRICE_ONBOARDING) is a retired name that fails boot if it is still set.

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

Platform Emails

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

VariableRequiredDescriptionExample
SENDLY_PLATFORM_SEND_API_KEYYesAPI key Sendly sends its OWN platform mail with.pk_...
SENDLY_FROM_ADDRESSYesFrom address used for platform notification emails.noreply@yourdomain.com

Notifications (ntfy)

Sendly 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/sendly-notifications

Security

VariableRequiredDescriptionDefault
EMAIL_RATE_LIMIT_PER_SECONDNoOverride the email sending rate limit, in emails per second. If not set, Sendly automatically fetches the quota from your AWS SES account. When set it is a hard ceiling — it applies whether the SES quota is higher or lower. Fractional values below 1 are supported for deliberately slow sending: 0.16 paces roughly one email every 6.25 seconds (~48 per 5 minutes), which is useful while warming up a new SES account or rebuilding sender reputation. Must be greater than 0.—

Phishing Detection

Sendly uses AI to detect and block phishing emails before they're sent. This requires an OpenRouter API key — the app fails at startup without one, so that content scanning can never be silently disabled.

VariableRequiredDescriptionDefault
OPENROUTER_API_KEYYesOpenRouter API key. Powers outbound phishing detection and inbound spam classification.—
OPENROUTER_ASSISTANT_MODELNoLLM model for the AI assistant. See OpenRouter models.anthropic/claude-3-haiku
OPENROUTER_MODERATION_MODELNoClassification model for phishing/spam scanning. Avoid reasoning models (they can return empty content).deepseek/deepseek-v4-flash
PHISHING_SAMPLE_RATE_DEFAULTNoPercentage 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.85
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_SAMPLE_RATE_DEFAULT) 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.

On this page