Skip to content

Environment reference

OpenPartner follows the principle that env is for secrets and build-time config; runtime content lives in the admin UI. So this list is shorter than you’d expect from competing products. .env.example in the repo lists every variable, including the ones only OpenPartner’s hosted service uses.

Required

VariableDescription
DATABASE_URLPostgres connection string. Both the API and the router need it.
SECRETS_ENCRYPTION_KEY32 bytes, hex or base64. Encrypts the SMTP passwords, Postmark tokens and other secrets stored from the admin UI. The API won’t start in production without it.
PORTAL_URLPublic URL of the portal, e.g. https://partners.yourdomain.com. It’s the API’s CORS allowlist and the base of emailed invite and sign-in links. The API won’t start in production without it.

Generate a key with:

Terminal window
openssl rand -hex 32

Optional

VariableDescription
OPENPARTNER_MODEselfhost (default), flat, or revshare. Gates billing layer. Self-hosted installs stay on selfhost; the other two are OpenPartner’s hosted billing modes.
OPENPARTNER_TENANCYsingle (default) or multi. multi hosts several brands on one install, each under /t/<slug>/, and won’t start without DATABASE_URL_APP.
DATABASE_URL_APPConnection string for the openpartner_app Postgres role, which row-level security applies to. Required with OPENPARTNER_TENANCY=multi. With single, leave it unset and the API uses DATABASE_URL. An empty value doesn’t count as unset.
OPENPARTNER_APP_DB_PASSWORDPassword for the openpartner_app role. The API creates the role, or updates its password, each time it starts. Set it together with DATABASE_URL_APP.
OPENPARTNER_ENABLE_SCHEDULER1 turns on the API’s in-process scheduler: commission auto-approval, scheduled payouts, housekeeping. Anything else leaves it off. docker-compose.prod.yml defaults it to 1. Run it on one API process only. See Scheduled jobs.
ADMIN_API_KEYBootstrap / CI bearer. Bypasses Admin user, behaves like a headless admin.
COOKIE_DOMAINRead by the router. Domain of the _cref attribution cookie: set it to your site’s domain with a leading dot (.yourdomain.com) so the cookie reaches the pages where you run the SDK. Default: localhost. (docker-compose.prod.yml sets .<OPENPARTNER_DOMAIN> when you leave it unset.)
CORS_EXTRA_ORIGINSOrigins the API accepts besides PORTAL_URL, comma-separated.
METRICS_TOKENBearer token required on /metrics. Unset, /metrics is open, so set it whenever the API is reachable from the internet.
API_PORT / ROUTER_PORTPorts the API and router listen on. Defaults: 4601 / 4701.
VELOCITY_MAX / VELOCITY_WINDOW_MSHow many clicks one IP can make on one link per window before the rest are flagged for fraud review. Default: 20 per 60,000 ms.

Mail

Mail settings saved in the admin UI (Settings → Email delivery, or the install wizard) win. These are fallbacks for when the UI has none.

VariableDescription
MAIL_FROMDefault From: address. Quote it if it has a display name: "Acme Partners <[email protected]>".
POSTMARK_SERVER_TOKENPostmark API token.
POSTMARK_MESSAGE_STREAMPostmark message stream. Default: outbound.
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASSWORDSMTP fallback. Port defaults to 587.
SMTP_SECURE1 to connect over TLS. Port 465 always does.

Stripe

All optional on a self-hosted install. Set them to your own Stripe account to pay partners over Stripe Connect and take in Stripe revenue through /webhooks/stripe. Leave them blank to pay partners by hand.

VariableDescription
STRIPE_SECRET_KEYYour Stripe secret key. Connect payouts are transfers from this account’s balance.
STRIPE_WEBHOOK_SECRET_PLATFORMSigning secret(s) for the event destination that receives your account’s own events (checkouts, invoices, charges, transfers). Comma-separated if you have several.
STRIPE_WEBHOOK_SECRET_CONNECTSigning secret(s) for the event destination that receives connected-account events (account.updated). Comma-separated if you have several.
STRIPE_WEBHOOK_SECRETLegacy. A single secret, or a comma-separated list, accepted for both destinations when the two variables above are unset. It works, but an event isn’t checked against the destination it should arrive on.

The other STRIPE_* variables in .env.example, such as the plan price IDs, configure OpenPartner’s hosted plans. Leave them empty.

Outbound webhooks

Webhook deliveries and partner postbacks only go to public IP addresses. These two variables loosen that.

VariableDescription
OPENPARTNER_OUTBOUND_ALLOWED_PORTSPorts deliveries may connect to, comma-separated. Unset: any port on a single-tenant self-host, 80 and 443 otherwise.
OPENPARTNER_OUTBOUND_ALLOW_PRIVATE_CIDRSPrivate or loopback ranges deliveries may reach anyway, as comma-separated CIDRs (e.g. to test against a local service). Only allowed with OPENPARTNER_MODE=selfhost and OPENPARTNER_TENANCY=single; in any other mode the API refuses to start with it set.

File uploads (brand logos, creator avatars)

Two backends, picked by OPENPARTNER_STORAGE_KIND. Default is fs — a local directory served at /uploads/*. Switch to s3 if your runtime is ephemeral (DO App Platform, Fly Machines, Heroku, etc.) or you just want a managed object store.

fs (default)

VariableDescription
OPENPARTNER_STORAGE_KINDfs (default).
OPENPARTNER_STORAGE_FS_DIRWhere to write files. Default: /var/lib/openpartner/uploads. Mount a volume here so uploads survive restarts, and make it writable by the API container’s node user (UID 1000). See below.
OPENPARTNER_STORAGE_FS_PUBLIC_BASEPublic URL base for the static handler. Default: ${API_URL}/uploads. Override if the API is behind a CDN or different hostname.
API_URLPublic URL of the API, used only to build the default above. Default: http://localhost:4601, so set this or OPENPARTNER_STORAGE_FS_PUBLIC_BASE in production.

The API image runs as node (UID 1000), not root, and doesn’t create /var/lib/openpartner. Docker creates a missing mount point owned by root, so the API can’t write to a fresh volume until you hand it to UID 1000. For a bind mount, chown -R 1000:1000 the host directory. For a named volume, run once:

Terminal window
docker run --rm -v <volume>:/data alpine chown -R 1000:1000 /data

s3 (any S3-compatible bucket — DO Spaces, AWS S3, Cloudflare R2, MinIO)

VariableDescription
OPENPARTNER_STORAGE_KINDs3.
OPENPARTNER_STORAGE_S3_BUCKETBucket name.
OPENPARTNER_STORAGE_S3_REGIONRegion (e.g. nyc3 for DO, us-east-1 for AWS).
OPENPARTNER_STORAGE_S3_ENDPOINTCustom endpoint URL. Required for non-AWS providers (e.g. https://nyc3.digitaloceanspaces.com). Omit for AWS S3.
OPENPARTNER_STORAGE_S3_ACCESS_KEY_IDAccess key ID.
OPENPARTNER_STORAGE_S3_SECRET_ACCESS_KEYSecret access key.
OPENPARTNER_STORAGE_S3_PUBLIC_BASEOptional CDN URL base (e.g. https://uploads.cdn.openpartner.dev). When unset, URLs are constructed from endpoint + bucket.

Uploads are written public-read with a 1-year immutable cache header. Object keys are random hex (16 bytes), so directory enumeration isn’t possible — but make sure the bucket itself doesn’t allow listing.

What’s NOT in env

These all live in the database and are managed from the admin UI:

  • Program name (the brand partners see)
  • Support email
  • Active mail transport (Postmark vs. SMTP vs. none)
  • Attribution model + lookback window (set per program)
  • Commission rules

Why? You can rotate them without redeploying, and self-host customers don’t have to manage config in two places.