Skip to content

Configuration

Plugwerk is configured via environment variables. All Plugwerk-specific variables use the PLUGWERK_ prefix.

The tables on this page curate the most-asked-about variables with extra commentary (security warnings, migration notes, examples). For the complete annotated reference — including every optional variable with its default — see the full .env.example reference at the bottom of this page; that block is auto-synced from the upstream .env.example on every release.

| Variable | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | PLUGWERK_AUTH_JWT_SECRET | HMAC signing key for JWT tokens (min 32 characters) | | PLUGWERK_AUTH_ENCRYPTION_KEY | Password used to derive the AES-256 key that encrypts stored OIDC client secrets. PBKDF2-derived, not a raw key. Min 16 / recommended 32+ characters. |

Plugwerk derives a 256-bit AES key from this value using PBKDF2WithHmacSHA1 (1024 iterations, deterministic 8-byte salt). The cipher is AES-256-CBC with a 16-byte random IV per ciphertext, implemented via Spring's AesBytesEncryptor.

The value you set is therefore a password, not a raw AES key — any length from 16 to 256 characters is accepted.

| Variable | Default | Description | | ---------------------- | ------------------------------------------- | ------------------- | | PLUGWERK_DB_URL | jdbc:postgresql://localhost:5432/plugwerk | JDBC connection URL | | PLUGWERK_DB_USERNAME | plugwerk | Database username | | PLUGWERK_DB_PASSWORD | plugwerk | Database password |

Example:

Terminal window
export PLUGWERK_DB_URL=jdbc:postgresql://db:5432/plugwerk
export PLUGWERK_DB_USERNAME=plugwerk
export PLUGWERK_DB_PASSWORD=secret

The defaults are tuned for managed Postgres behind a client-facing connection pooler — Supabase Supavisor, AWS RDS Proxy, or PgBouncer in transaction mode — where the pooler already multiplexes thousands of client connections onto a small backend pool. Plugwerk's own JDBC pool therefore stays small and connections short-lived. On shared tiers like Supabase Free, raising these values will quickly exhaust the project-wide client-connection budget and produce FATAL: Max client connections reached.

For a dedicated Postgres instance with no pooler in front, raising PLUGWERK_DB_POOL_MAX_SIZE to 1020 is appropriate.

| Variable | Default | Description | | ---------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- | | PLUGWERK_DB_POOL_MAX_SIZE | 5 | Maximum number of pooled JDBC connections. | | PLUGWERK_DB_POOL_MIN_IDLE | 1 | Minimum number of idle connections kept warm. | | PLUGWERK_DB_POOL_IDLE_TIMEOUT_MS | 30000 (30 s) | Idle connections older than this are released back to the pooler. | | PLUGWERK_DB_POOL_MAX_LIFETIME_MS | 600000 (10 min) | Hard cap on a single physical connection's lifetime. Keep below the pooler's own timeout to avoid mid-query drops. | | PLUGWERK_DB_POOL_CONNECTION_TIMEOUT_MS | 30000 (30 s) | Fail fast if a connection cannot be acquired within this window. |

Use the Supavisor pooler endpoint, not the direct database host. When using Supavisor in transaction mode, append ?prepareThreshold=0 to the JDBC URL — server-side prepared statements do not survive transaction-pooled connections and will fail with prepared statement "S_1" does not exist.

Terminal window
export PLUGWERK_DB_URL='jdbc:postgresql://aws-0-eu-central-1.pooler.supabase.com:6543/postgres?prepareThreshold=0'
export PLUGWERK_DB_USERNAME='postgres.<project-ref>'
export PLUGWERK_DB_PASSWORD='<your-password>'
# Keep the defaults — do not raise PLUGWERK_DB_POOL_MAX_SIZE on a shared project.

A single-tenant Postgres instance with no pooler in front can sustain a larger Plugwerk-side pool:

Terminal window
export PLUGWERK_DB_POOL_MAX_SIZE=20
export PLUGWERK_DB_POOL_MIN_IDLE=2

MAX_LIFETIME_MS and IDLE_TIMEOUT_MS can usually stay at their defaults — they are not the bottleneck without a pooler.

Plugwerk persists plugin artifacts via a pluggable backend — local filesystem (fs, default) or S3-compatible object storage (s3). The backend is selected via PLUGWERK_STORAGE_TYPE. For the full configuration surface (all PLUGWERK_STORAGE_* variables), per-provider examples (AWS S3, MinIO, Cloudflare R2, Hetzner Object Storage), and migration strategies between backends, see Storage backends.

Plugwerk's frontend is bundled into the server JAR and served from the same origin as the REST API, so the default behaviour is same-origin-only — no cross-origin requests are accepted. Operators only need to configure CORS when the frontend is deployed on a separate origin (CDN, subdomain, etc.).

| Variable | Default | Description | | ---------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | PLUGWERK_SERVER_CORS_ALLOWED_ORIGINS | (empty = same-origin-only) | Comma-separated exact origin values. Case-sensitive; scheme and host must match exactly. Wildcards are not supported here — list every origin explicitly. | | PLUGWERK_SERVER_CORS_ALLOWED_METHODS | GET,POST,PUT,PATCH,DELETE,OPTIONS | Comma-separated HTTP methods browsers may use in cross-origin requests. | | PLUGWERK_SERVER_CORS_ALLOWED_HEADERS | Authorization,Content-Type,X-Api-Key | Comma-separated request headers browsers may send on cross-origin requests. | | PLUGWERK_SERVER_CORS_ALLOW_CREDENTIALS | true | Whether browsers may include credentials (cookies, Authorization header). Required for JWT Bearer auth from a different origin. | | PLUGWERK_SERVER_CORS_MAX_AGE | 3600 | Preflight cache duration in seconds. Bounded at 0..86400. |

Plugwerk's per-IP rate limiting (login brute-force protection) keys off the connecting client's IP. When a reverse proxy fronts the server, every request appears to come from the proxy — without configuration, all clients share a single rate-limit bucket and the protection collapses.

| Variable | Default | Description | | ----------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------- | | PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS | (empty) | Comma-separated CIDR ranges. X-Forwarded-For is honoured only for connections originating inside one of these. |

The default — empty list — means X-Forwarded-For is ignored and the connection's remoteAddr is used. This is correct for a Plugwerk instance that clients reach directly. When a proxy is in front, the variable must be set; see Running behind a reverse proxy for stack-specific examples (nginx, Kubernetes ingress, AWS ALB) and the rationale.

Entries use standard CIDR notation (10.0.0.0/8, 127.0.0.1/32, ::1/128). A bare IP without a prefix is accepted and treated as /32 (IPv4) or /128 (IPv6). Each entry is parsed at startup; an unparseable value aborts the boot with an indexed error pointing at the offending entry.

| Variable | Default | Description | | ----------------------------------------- | ---------- | -------------------------------------------------------------------------------------------- | | PLUGWERK_AUTH_ADMIN_PASSWORD | (random, written to stderr + /tmp/plugwerk-admin-password.txt (0600), passwordChangeRequired) | Fixed initial superadmin password. When unset, the server generates one and writes it to stderr and /tmp/plugwerk-admin-password.txt (mode 0600), bypassing SLF4J so log aggregators do not capture it. See Authentication. | | PLUGWERK_AUTH_ACTUATOR_SCRAPE_USERNAME | (unset) | Username for the optional Prometheus scrape account. Must be set together with the password. | | PLUGWERK_AUTH_ACTUATOR_SCRAPE_PASSWORD | (unset) | Plaintext password for the scrape account (BCrypt-encoded at startup). Min 16 chars; 32+ recommended. | | PLUGWERK_TRACKING_ENABLED | true | Enable download event audit log | | PLUGWERK_TRACKING_CAPTURE_IP | true | Capture client IP in download events | | PLUGWERK_TRACKING_ANONYMIZE_IP | true | Anonymize IPs to /24 (IPv4) or /48 (IPv6) | | PLUGWERK_TRACKING_CAPTURE_USER_AGENT | true | Capture User-Agent header | | PLUGWERK_AUTH_OIDC_ALLOW_PRIVATE_DISCOVERY_URIS | false | OIDC SSRF escape hatch — accepts private/loopback/link-local hosts. Local development only. See OIDC SSRF guard. | | PLUGWERK_AUTH_OIDC_BLOCKED_HOST_NAMES | (default list) | Comma-separated exact-match host blocklist. Replaces the defaults entirely — see OIDC SSRF guard. | | PLUGWERK_AUTH_OIDC_BLOCKED_HOST_SUFFIXES| (default list) | Comma-separated suffix blocklist. Replaces the defaults entirely — see OIDC SSRF guard. | | PLUGWERK_AUTH_OIDC_FAIL_FAST_ON_UNDECRYPTABLE_PROVIDERS | false | Refuse to start when any enabled OIDC provider's client secret cannot be decrypted with the current encryption key. See OIDC encryption-key safety. |

When the scrape variables are left unset, /actuator/info and /actuator/prometheus require a superadmin JWT. See Monitoring for the full setup, including a Prometheus scrape_configs example.

Plugwerk validates every OIDC discovery URL and every stored issuer / endpoint URI through a host classifier that rejects hosts attackers might use to coerce the server into making outbound requests against trusted private targets. The check runs both at write time (admin form / REST POST /admin/oidc-providers) and at read time (registry refresh).

| Class | How | | ------------------------------------------ | ----------------------------------------------------------------------------------------- | | Private IPv4 ranges (RFC 1918) | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16hardcoded, not configurable | | Loopback | 127.0.0.0/8, ::1hardcoded | | Link-local (incl. cloud metadata services) | 169.254.0.0/16 (covers AWS / GCP / Azure metadata at 169.254.169.254) — hardcoded | | ULA IPv6 | fc00::/7hardcoded | | IPv4-mapped IPv6 of any of the above | ::ffff:0:0/96 mapping — hardcoded | | Hostname blocklist (exact match) | localhost, metadata.google.internal, metadata — configurable | | Hostname blocklist (suffix match) | .localhost, .local, .lan, .internal — configurable |

The IP-range checks have no configuration switch. The only way to relax them is the brute-force escape hatch below.

PLUGWERK_AUTH_OIDC_BLOCKED_HOST_NAMES and PLUGWERK_AUTH_OIDC_BLOCKED_HOST_SUFFIXES accept a comma-separated list. Suffix entries without a leading . get one prepended (so corp.example.com matches idp.corp.example.com but not evilcorp.example.com).

Example — drop .internal because it is a legitimate public domain in your environment:

Terminal window
PLUGWERK_AUTH_OIDC_BLOCKED_HOST_SUFFIXES=.localhost,.local,.lan
# .internal intentionally absent from the union

Example — add a corporate suffix on top of the defaults:

Terminal window
PLUGWERK_AUTH_OIDC_BLOCKED_HOST_SUFFIXES=.localhost,.local,.lan,.internal,.corp.example.com
# full union spelled out

The escape hatch (PLUGWERK_AUTH_OIDC_ALLOW_PRIVATE_DISCOVERY_URIS)

Section titled “The escape hatch (PLUGWERK_AUTH_OIDC_ALLOW_PRIVATE_DISCOVERY_URIS)”

Setting this to true disables every IP-range and hostname check. The single use case is local development against a Keycloak / mock-oauth2-server bound to localhost:

Terminal window
PLUGWERK_AUTH_OIDC_ALLOW_PRIVATE_DISCOVERY_URIS=true

The server emits a WARN line at startup whenever the escape hatch is on:

plugwerk.auth.oidc.allow-private-discovery-uris=true — OIDC SSRF guard is …

Production must keep this false. A misconfigured production instance with true here is one malicious provider registration away from the server hitting your cloud's instance metadata endpoint on behalf of the attacker.

For the operator-facing OIDC provider setup that interacts with these checks, see OIDC / OAuth 2.0 Providers.

Plugwerk encrypts every OIDC provider's clientSecret at rest using PLUGWERK_AUTH_ENCRYPTION_KEY. When that key changes — rotation, deployment-environment switch, DB restore, fresh DB import with stale secrets — secrets stored under the previous key cannot be decrypted, and Sign-in via those providers stops working at runtime.

PLUGWERK_AUTH_OIDC_FAIL_FAST_ON_UNDECRYPTABLE_PROVIDERS (default false) controls how the server reacts to that condition.

The server boots, every affected provider is logged at ERROR, and Sign-in via those providers returns 4xx at runtime. Other providers continue working. The log line is structured so it is easy to grep:

ERROR Cannot decrypt OIDC provider 'Google' (registrationId=…) client secret. The data was
encrypted with a different key than the current PLUGWERK_AUTH_ENCRYPTION_KEY. Re-enter
the client secret in the Admin UI under Admin → OIDC Providers, or restore the previous
PLUGWERK_AUTH_ENCRYPTION_KEY value.

This is appropriate for development environments where one broken provider should not block iteration on the rest.

Terminal window
PLUGWERK_AUTH_OIDC_FAIL_FAST_ON_UNDECRYPTABLE_PROVIDERS=true

When set, the server refuses to start if any enabled provider's secret cannot be decrypted. The startup error message lists every affected registrationId in one shot, so a single boot attempt yields the complete remediation list:

Refusing to start: 2 enabled OIDC provider(s) have an undecryptable
client_secret_encrypted under the current PLUGWERK_AUTH_ENCRYPTION_KEY
(registrationIds=[…]). Either restore the previous key or re-enter the client
secret(s) in the Admin UI, then restart. Set
plugwerk.auth.oidc.fail-fast-on-undecryptable-providers=false to allow startup
with affected providers skipped instead.

For the operator-facing fix path (re-entering the client secret), see Managing existing providers. For the symptom-side troubleshooting entry, see Sign-in returns 4xx but the server is healthy.

The canonical defaults and inline operator notes for both the SSRF guard and the encryption-key safety flag are also shipped in the upstream .env.example, mirrored verbatim below:

Terminal window
# Dev/test escape hatch for the OIDC SSRF guard. When true, private,
# loopback, link-local, and metadata hosts are accepted in issuer URIs.
# Required when running Keycloak / mock-oauth2-server on `localhost`.
# **NEVER set this in production** — the server logs a startup WARN whenever
# this is true so an accidental flip is visible in production logs.
# PLUGWERK_AUTH_OIDC_ALLOW_PRIVATE_DISCOVERY_URIS=false
# Comma-separated exact-match host blocklist. Empty default = use the
# hardcoded list: localhost, metadata.google.internal, metadata.
# A non-empty value REPLACES the defaults entirely — drop entries from the
# override if your operator legitimately uses them.
# PLUGWERK_AUTH_OIDC_BLOCKED_HOST_NAMES=
# Comma-separated suffix blocklist. Empty default = use the hardcoded list:
# .localhost, .local, .lan, .internal. A non-empty value REPLACES the defaults
# entirely. Suffixes without a leading `.` get one prepended so
# `corp.example.com` matches `idp.corp.example.com` but not
# `evilcorp.example.com`.
# PLUGWERK_AUTH_OIDC_BLOCKED_HOST_SUFFIXES=
# Refuse to start if any ENABLED OIDC provider's client_secret_encrypted
# cannot be decrypted with the current PLUGWERK_AUTH_ENCRYPTION_KEY (#501).
# Default false → server boots, affected providers skip with an ERROR log
# line, Sign-in via that provider silently 4xx's. Set true in production
# where a half-broken login surface is worse than a fail-closed restart loop.
# PLUGWERK_AUTH_OIDC_FAIL_FAST_ON_UNDECRYPTABLE_PROVIDERS=false

The public health endpoint is /actuator/health — no authentication required, suitable for container healthchecks and uptime probes. For Prometheus metrics and the authentication contract on /actuator/info and /actuator/prometheus, see Monitoring.

The complete annotated environment-variable template, mirrored from the upstream plugwerk/plugwerk repo on every release. Every variable above is also covered here with its default value and inline operator notes. Operators can copy the whole block as a starting .env file:

.env.example
# Plugwerk — Environment Variables
# Copy this file to .env and fill in the values.
# .env is git-ignored and will never be committed.
# --- REQUIRED (server will refuse to start without these) ---
# HMAC-SHA256 signing key for JWT tokens. Minimum 32 characters.
# Generate with: openssl rand -base64 32
PLUGWERK_AUTH_JWT_SECRET=
# Password for the AES-256-CBC text encryptor that protects OIDC client secrets
# at rest. PBKDF2-derived key, so length controls input entropy (NOT AES key size).
# Minimum 16 characters; 32+ recommended. See docs/adrs/0022-encryption-key-size.md.
# Rotating this value invalidates every existing OIDC client secret in the database.
# Generate with: openssl rand -base64 32 (recommended)
# or: openssl rand -hex 8 (legacy minimum)
PLUGWERK_AUTH_ENCRYPTION_KEY=
# --- OPTIONAL (sensible defaults are provided) ---
# Fixed initial admin password. When set, the admin account uses this password
# and skips random generation. When absent, a random password is generated and
# surfaced on two channels that bypass SLF4J — container stderr and
# /tmp/plugwerk-admin-password.txt (POSIX 0600). Log aggregators
# (Datadog/ELK/CloudWatch) do NOT capture the credential. The admin username
# is always "admin".
# PLUGWERK_AUTH_ADMIN_PASSWORD=
# Refresh-cookie `Secure` flag. Default: true (HTTPS-only transport).
# **Set to false for local HTTP development.** Browsers silently drop `Secure`
# cookies over plain HTTP, which makes the refresh cookie invisible to the
# server on reload and forces the user back to /login. See ADR-0027.
# PLUGWERK_AUTH_COOKIE_SECURE=false
# --- Token validity ---
# Legacy self-issued JWT access-token lifetime in hours. Default 8.
# PLUGWERK_TOKEN_VALIDITY_HOURS=8
# Short-lived access-token lifetime in minutes. 1..1440. Default 15.
# PLUGWERK_AUTH_ACCESS_TOKEN_VALIDITY_MINUTES=15
# Refresh-token lifetime in hours. 1..8760 (one year). Default 168 (7 days).
# PLUGWERK_AUTH_REFRESH_TOKEN_VALIDITY_HOURS=168
# Comma-separated hostnames (no scheme, no port) that the artifact download
# endpoint may proxy through. Empty default = downloads served from the
# server's own origin only.
# PLUGWERK_AUTH_DOWNLOAD_ALLOWED_HOSTS=
# Reverse-proxy trust list for X-Forwarded-For (SBS-006 / #265).
# Comma-separated CIDR ranges of trusted proxies. The leftmost X-Forwarded-For
# value is honoured ONLY when the request's remoteAddr matches one of these
# ranges. Empty default = X-Forwarded-For is ignored entirely.
# **WARNING for operators behind nginx / ALB / Traefik / CloudFront:** leaving
# this empty causes every client to be rate-limited as if it were the proxy IP,
# collapsing per-IP rate-limit buckets to one shared bucket. You MUST set this
# to your proxy's egress IPs:
# PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS=10.0.0.0/8,127.0.0.1/32
# PLUGWERK_AUTH_TRUSTED_PROXY_CIDRS=
# --- Brute-force rate limiting for authentication endpoints ---
# Token-bucket via Bucket4j + Caffeine. Exceeding a limit returns HTTP 429
# with a Retry-After header.
# Login attempts per client IP within the window. Default: 10 attempts / 60s.
# PLUGWERK_AUTH_RATE_LIMIT_MAX_ATTEMPTS=10
# PLUGWERK_AUTH_RATE_LIMIT_WINDOW_SECONDS=60
# Change-password attempts per authenticated subject (independent bucket from
# the login limit above). Default: 5 attempts / 300s.
# PLUGWERK_AUTH_RATE_LIMIT_CHANGE_PASSWORD_MAX_ATTEMPTS=5
# PLUGWERK_AUTH_RATE_LIMIT_CHANGE_PASSWORD_WINDOW_SECONDS=300
# Self-registration rate limits. IP bucket and email bucket are independent so a
# single abuser cannot exhaust the per-email cap for a victim's address.
# Defaults: 10 IP-attempts / 60s, 5 email-attempts / 60s (email is SHA-256-hashed
# before bucketing — the address itself is never used as a cache key).
# PLUGWERK_AUTH_RATE_LIMIT_REGISTER_IP_MAX_ATTEMPTS=10
# PLUGWERK_AUTH_RATE_LIMIT_REGISTER_IP_WINDOW_SECONDS=60
# PLUGWERK_AUTH_RATE_LIMIT_REGISTER_EMAIL_MAX_ATTEMPTS=5
# PLUGWERK_AUTH_RATE_LIMIT_REGISTER_EMAIL_WINDOW_SECONDS=60
# Password-reset rate limits. IP bucket caps reset-request bursts; token bucket
# caps consume-attempts per reset-token-hash. Defaults: 5 IP-attempts / 900s,
# 10 token-attempts / 3600s.
# PLUGWERK_AUTH_RATE_LIMIT_PASSWORD_RESET_IP_MAX_ATTEMPTS=5
# PLUGWERK_AUTH_RATE_LIMIT_PASSWORD_RESET_IP_WINDOW_SECONDS=900
# PLUGWERK_AUTH_RATE_LIMIT_PASSWORD_RESET_TOKEN_MAX_ATTEMPTS=10
# PLUGWERK_AUTH_RATE_LIMIT_PASSWORD_RESET_TOKEN_WINDOW_SECONDS=3600
# --- OIDC hardening (#479 SSRF guard + #501 startup safety) ---
# Dev/test escape hatch for the OIDC SSRF guard. When true, private,
# loopback, link-local, and metadata hosts are accepted in issuer URIs.
# Required when running Keycloak / mock-oauth2-server on `localhost`.
# **NEVER set this in production** — the server logs a startup WARN whenever
# this is true so an accidental flip is visible in production logs.
# PLUGWERK_AUTH_OIDC_ALLOW_PRIVATE_DISCOVERY_URIS=false
# Comma-separated exact-match host blocklist. Empty default = use the
# hardcoded list: localhost, metadata.google.internal, metadata.
# A non-empty value REPLACES the defaults entirely — drop entries from the
# override if your operator legitimately uses them.
# PLUGWERK_AUTH_OIDC_BLOCKED_HOST_NAMES=
# Comma-separated suffix blocklist. Empty default = use the hardcoded list:
# .localhost, .local, .lan, .internal. A non-empty value REPLACES the defaults
# entirely. Suffixes without a leading `.` get one prepended so
# `corp.example.com` matches `idp.corp.example.com` but not
# `evilcorp.example.com`.
# PLUGWERK_AUTH_OIDC_BLOCKED_HOST_SUFFIXES=
# Refuse to start if any ENABLED OIDC provider's client_secret_encrypted
# cannot be decrypted with the current PLUGWERK_AUTH_ENCRYPTION_KEY (#501).
# Default false → server boots, affected providers skip with an ERROR log
# line, Sign-in via that provider silently 4xx's. Set true in production
# where a half-broken login surface is worse than a fail-closed restart loop.
# PLUGWERK_AUTH_OIDC_FAIL_FAST_ON_UNDECRYPTABLE_PROVIDERS=false
# --- Server & storage ---
# Backend base URL — used to build OAuth2 redirect URIs and verification
# links. Default: http://localhost:8080.
# PLUGWERK_BASE_URL=http://localhost:8080
# Frontend base URL — emitted in browser-facing links (verification emails,
# password-reset links). Empty default = same as PLUGWERK_BASE_URL (bundled
# SPA case). Override for split-deployment dev: Vite on a different port.
# PLUGWERK_WEB_BASE_URL=http://localhost:5173
# PLUGWERK_WEB_BASE_URL=
# Storage backend selector — `fs` (default, local filesystem) or `s3`
# (S3-compatible object storage; AWS S3 / MinIO / Hetzner / Cloudflare R2).
# PLUGWERK_STORAGE_TYPE=fs
# PLUGWERK_STORAGE_ROOT=/var/plugwerk/artifacts
# --- S3-compatible storage (only used when PLUGWERK_STORAGE_TYPE=s3) ---
# Required:
# PLUGWERK_STORAGE_S3_BUCKET=plugwerk-artifacts
# PLUGWERK_STORAGE_S3_REGION=eu-central-1
# Optional endpoint override — leave blank for AWS S3, set for MinIO / R2 /
# Hetzner Object Storage:
# MinIO local: http://localhost:9000
# Cloudflare R2: https://<account>.r2.cloudflarestorage.com
# Hetzner OS: https://fsn1.your-objectstorage.com
# PLUGWERK_STORAGE_S3_ENDPOINT=
# Static credentials — leave BOTH blank to use the AWS DefaultCredentialsProvider
# chain (env, instance profile, IRSA, ECS task role). Half-configured states
# (one set, one blank) are rejected at startup.
# PLUGWERK_STORAGE_S3_ACCESS_KEY=
# PLUGWERK_STORAGE_S3_SECRET_KEY=
# Optional prefix prepended to every artifact key — lets multiple Plugwerk
# installations share one bucket. Must not start with '/'.
# PLUGWERK_STORAGE_S3_KEY_PREFIX=
# `true` for MinIO and other endpoints that need path-style URLs. Default
# `false` (DNS-style; AWS S3, R2, Hetzner all work with this).
# PLUGWERK_STORAGE_S3_PATH_STYLE_ACCESS=false
# Refuse to start when the bucket-existence probe fails. Default `false` →
# probe failure logs ERROR and the server keeps running. Set `true` for
# orchestrators that prefer a restart loop over a half-broken artifact store.
# PLUGWERK_STORAGE_S3_FAIL_FAST_ON_BUCKET_MISSING=false
# --- Storage consistency check (#190) ---
# Circuit breaker for the admin storage-reconciliation scan. Buckets larger
# than this short-circuit with HTTP 409 instead of blocking the admin UI.
# Default 100000.
# PLUGWERK_STORAGE_CONSISTENCY_MAX_KEYS_PER_SCAN=100000
# --- Orphan-artifact reaper (#496) ---
# Periodic cleanup job that removes storage objects with no plugin_release
# row, after a grace period. ShedLock-coordinated across instances. See
# docs/adrs/0035-shedlock-scheduler-coordination.md.
# PLUGWERK_STORAGE_REAPER_ENABLED=true
# Spring cron: sec min hour day-of-month month day-of-week. Default 03:15.
# PLUGWERK_STORAGE_REAPER_CRON=0 15 3 * * *
# When true, the reaper logs the keys it WOULD delete without touching
# storage. Flip to false once you've validated the eviction list for a
# release cycle.
# PLUGWERK_STORAGE_REAPER_DRY_RUN=true
# Minimum age (hours) of a storage object before the reaper considers it
# an orphan. Must be longer than your longest plausible publish transaction.
# PLUGWERK_STORAGE_REAPER_GRACE_PERIOD_HOURS=24
# Hard cap on deletes per run. The remainder rolls to the next tick.
# PLUGWERK_STORAGE_REAPER_MAX_DELETES_PER_TICK=1000
# --- Scheduler coordination ---
# ShedLock gate on every @Scheduled job — one Plugwerk instance per tick
# (ADR-0035). Keep enabled in production. Disable only in tests against an
# in-memory DB without the shedlock table.
# PLUGWERK_SCHEDULER_SHEDLOCK_ENABLED=true
# --- Database ---
# PLUGWERK_DB_URL=jdbc:postgresql://localhost:5432/plugwerk
# PLUGWERK_DB_USERNAME=plugwerk
# PLUGWERK_DB_PASSWORD=plugwerk
# HikariCP pool sizing. Defaults (5 / 1 / 30s / 600s / 30s) are tuned for a
# managed Postgres behind a connection pooler (e.g. Supabase Supavisor /
# PgBouncer in transaction mode). For a dedicated Postgres you can raise
# PLUGWERK_DB_POOL_MAX_SIZE.
# PLUGWERK_DB_POOL_MAX_SIZE=5
# PLUGWERK_DB_POOL_MIN_IDLE=1
# PLUGWERK_DB_POOL_IDLE_TIMEOUT_MS=30000
# PLUGWERK_DB_POOL_MAX_LIFETIME_MS=600000
# PLUGWERK_DB_POOL_CONNECTION_TIMEOUT_MS=30000
# --- CORS ---
# CORS — same-origin-only by default (frontend is bundled with the server JAR).
# Set this only if the frontend is deployed on a separate origin. For local dev
# with the Vite dev server on port 5173, uncomment the line below.
# See docs/adrs/0021-cors-same-origin-default.md for the full threat model.
# PLUGWERK_SERVER_CORS_ALLOWED_ORIGINS=http://localhost:5173
# PLUGWERK_SERVER_CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
# PLUGWERK_SERVER_CORS_ALLOWED_HEADERS=Authorization,Content-Type,X-Api-Key
# PLUGWERK_SERVER_CORS_ALLOW_CREDENTIALS=true
# PLUGWERK_SERVER_CORS_MAX_AGE=3600
# --- Operational ---
# Actuator scrape account — unattended Prometheus scraping of /actuator/info and
# /actuator/prometheus via HTTP Basic. Leave both blank to keep those endpoints
# superadmin-only. See docs/adrs/0025-actuator-endpoint-hardening.md.
# Set BOTH values together; half-configured states are rejected at startup.
# Generate password with: openssl rand -base64 32
# PLUGWERK_AUTH_ACTUATOR_SCRAPE_USERNAME=prometheus
# PLUGWERK_AUTH_ACTUATOR_SCRAPE_PASSWORD=

The same file is also served as a static asset at /config/env.example for direct curl / wget consumption.