Configuration
Every environment variable Keygate reads, what it does and its default: database, secrets, Stripe, email, Redis, rate limits, webhooks and release storage.
How settings are read
Keygate reads its settings from environment variables. When it starts, it also loads a .env file from its working folder, and real environment variables win over the file. With Docker Compose, everything in .env is passed to the container.
A few things, such as the email provider, the site name and which emails go out, can also be changed later in the dashboard under Settings. Those are stored in the database and do not need a restart.
Keygate checks its settings at startup and refuses to start if a required one is missing or clearly wrong, such as a signing key of the wrong length. The log says which one.
Server
| Variable | Default | What it does |
|---|---|---|
PORT | 9000 | Port the HTTP server listens on. |
BASE_URL | http://localhost:9000 | The public address of your install. Used in email links, after Stripe Checkout, as the Stripe webhook address and in the update feed addresses the dashboard shows. Use HTTPS in production. |
ENVIRONMENT | development | development, staging or production. Development turns on a login shortcut without email codes, meant for local work only. Production warns at startup when BASE_URL is not HTTPS or ADMIN_EMAILS is empty. |
DATABASE_URL | required | PostgreSQL connection string, for example postgres://keygate:secret@db:5432/keygate?sslmode=disable. |
Secrets
Generate each secret with openssl rand -hex 32. Store them somewhere safe; the backups section explains what happens if one is lost.
| Variable | Default | What it does |
|---|---|---|
JWT_SECRET | required | Signs login sessions. At least 32 characters. |
LICENSE_SIGNING_KEY | required | Ed25519 key that signs the offline tokens your app verifies. Exactly 64 hex characters. |
RELEASE_KEY_ENCRYPTION_KEY | empty | Encrypts release signing keys, email provider credentials and stored license keys. Required when release storage is on, and to save SMTP or Cloudflare credentials in the dashboard. |
Stripe
| Variable | Default | What it does |
|---|---|---|
STRIPE_SECRET_KEY | empty | Your Stripe secret or restricted key. When set, Keygate creates its own webhook endpoint in Stripe, so nothing else is needed. |
STRIPE_WEBHOOK_SECRET | empty | Only needed when you forward events yourself, for example with the Stripe CLI during development. See local development. |
Without email settings Keygate still runs, and login codes are written to the server log. You can set SMTP here or pick a provider in the dashboard. More in Emails.
| Variable | Default | What it does |
|---|---|---|
SMTP_HOST | empty | SMTP server host name. |
SMTP_PORT | 587 | SMTP port. |
SMTP_USERNAME | empty | SMTP user. |
SMTP_PASSWORD | empty | SMTP password. |
SMTP_FROM | empty | Sender address, either [email protected] or Acme Licensing <[email protected]>. |
Redis
| Variable | Default | What it does |
|---|---|---|
REDIS_URL | empty | Redis or Valkey address, such as redis://localhost:6379/0. Shares rate limits across instances. If Redis goes away, Keygate falls back to counting in memory until it is back. |
Rate limits
Limits are per client IP address.
| Variable | Default | What it does |
|---|---|---|
RATE_LIMIT_API | 60 | Base rate per minute. The license API allows twice this, at least 120, and update feeds four times, at least 240. |
RATE_LIMIT_ADMIN | 120 | Requests per minute to the admin API. |
RATE_LIMIT_AUTH | 60 | Login attempts per minute. People behind one office network share it. |
RATE_LIMIT_OTP_SEND | 30 | Login codes sent per hour. Kept low because this endpoint mails any address it is given. |
BF_MAX_FAILS | 5 | Failed license calls from one address within five minutes before it is locked out. |
BF_LOCKOUT_SECONDS | 30 | The first lockout. Each one after that lasts twice as long, up to 30 minutes. |
Webhooks
| Variable | Default | What it does |
|---|---|---|
WEBHOOK_MAX_ATTEMPTS | 5 | Delivery attempts before giving up. |
WEBHOOK_RETRY_INTERVAL | 30s | How often Keygate looks for deliveries that are due for a retry. |
WEBHOOK_HTTP_TIMEOUT | 10s | How long to wait for your endpoint. |
WEBHOOK_ALLOW_PRIVATE | false | Allow deliveries to private and local addresses, such as a receiver on the same Docker network. |
Release storage
Only needed if Keygate hosts your app's releases. Leave STORAGE_BUCKET empty to turn releases off; everything else works without it. The bucket, access key and secret key must be set together. See Shipping updates.
| Variable | Default | What it does |
|---|---|---|
STORAGE_ENDPOINT | empty | Empty for AWS S3, https://ACCOUNT.r2.cloudflarestorage.com for R2, http://minio:9000 for MinIO. |
STORAGE_REGION | auto | auto for R2, the bucket's region for S3. |
STORAGE_BUCKET | empty | Bucket name. |
STORAGE_ACCESS_KEY | empty | Access key. |
STORAGE_SECRET_KEY | empty | Secret key. |
STORAGE_FORCE_PATH_STYLE | false | Set to true for MinIO and most self hosted S3 gateways. |
STORAGE_UPLOAD_TTL | 1h | How long an upload link stays valid. |
STORAGE_DOWNLOAD_TTL | 10m | How long a license checked download link stays valid. |
STORAGE_FEED_URL_TTL | 24h | How long download links inside update feeds stay valid. Users may click Install long after the feed was fetched. |
MAX_RELEASE_SIGN_SIZE_MB | 500 | Largest file Keygate signs. Signing holds the whole file in memory. |
Other settings
| Variable | Default | What it does |
|---|---|---|
ADMIN_EMAILS | empty | Comma separated emails that become admins the first time they sign in. |
QUOTA_WARNING_THRESHOLD | 0.8 | Share of a usage quota at which the warning webhook and email go out. |
BASE_URL=https://licenses.example.com
JWT_SECRET=2f1c...c9a0
LICENSE_SIGNING_KEY=8be4...41d7
RELEASE_KEY_ENCRYPTION_KEY=5a90...e3b2
STRIPE_SECRET_KEY=sk_live_...
SMTP_HOST=smtp.example.com
[email protected]
SMTP_PASSWORD=...
SMTP_FROM=Acme Licensing <[email protected]>Last updated October 4, 2026