Skip to content

Configuration Reference

This page lists every environment variable the Productify platform reads, across the Manager, the Proxy (Caddyfile global block + environment), and the Pocket ID integration — with its default, whether it is required in development versus production, and its security implications.

Drift-tested

The Manager half of this page is verified in CI against manager/internal/config/config.go: if a new setting lands in the Manager without a row here, the build fails. See Keeping this page honest.

The Manager loads configuration through kkyr/fig from config.local.yml (falling back to config.yml, or the file named by PFY_CONFIG_PATH), and every value can be overridden by an environment variable. The variable name is the YAML key path, joined with _, uppercased, and prefixed with PFY_ — e.g. the YAML key db.host is PFY_DB_HOST.

Two defaults make the platform secure by default: env defaults to production, and in production the Manager refuses to start without a DB password, a Pocket ID API key, and an encryption key, and rejects sslmode=disable. Local development opts out of all of that explicitly with env: development.

Manager

Core

VariableDefaultDevProdNotes
PFY_ENVproductiondevelopmentproductionSecure by default. Gates the /gql playground, the dev reverse proxy, and the dev-user-override escape hatch (all IsDev()-only). Must be exactly development or production; anything else fails validation.
PFY_RUN_MODEbothanyapi, executor, or bothWhich halves of the Manager run in this process: the API server, the trigger executor, or both.
PFY_PORT8080Main HTTP/API port. Must differ from PFY_HEALTH_PORT; range 1–65535.
PFY_HEALTH_PORT8081Health-check port (/health). Must differ from PFY_PORT.
PFY_FRONTEND_DIR/app/frontendDirectory the Manager serves the bundled frontend from.
PFY_PROXY_TARGEThttp://localhost:5173Dev-only reverse-proxy target for the Vite frontend dev server; used only when IsDev().
PFY_CONFIG_PATH(unset)optionaloptionalOverrides which config file is loaded, ahead of the config.local.ymlconfig.yml search.

Security & secrets

VariableDefaultDevProdNotes
PFY_PROXY_SECRET(empty)recommendedrequiredThe shared secret the trusted proxy sends as X-Proxy-Auth on every forwarded request. It establishes the proxy↔manager trust boundary — without it the Manager would trust identity headers (X-Token-*) from anyone who can reach its port. Empty means "no secret configured": outside the dev-user-override escape hatch the Manager fails closed (rejects every request). Must match the proxy's PFY_MANAGER_TOKEN.
PFY_ENCRYPTION_KEY(empty)optional (warns)requiredBase64-encoded 32-byte (AES-256) key encrypting secret columns at rest (endpoint auth_password/auth_token, registered-backend token). Unset in production ⇒ the Manager refuses to start (secrets would be plaintext). Unset in development ⇒ those columns are stored unencrypted with a loud warning. A malformed key is a fatal error in any environment.

Database (db.*)

VariableDefaultDevProdNotes
PFY_DB_HOSTlocalhostPostgreSQL host.
PFY_DB_PORT5432PostgreSQL port; range 1–65535.
PFY_DB_USERpostgresPostgreSQL user.
PFY_DB_PASSWORD(no default)may be emptyrequiredHas no default on purpose: a production deployment must supply one explicitly rather than silently shipping the well-known postgres credential. Validation rejects an empty password in production. Never logged (kept out of DSN/URL error text).
PFY_DB_NAMEpostgresDatabase name.
PFY_DB_SSLMODErequiredisable (opt-in)require or strongerSecure by default. disable is rejected in production; local development opts out explicitly.
PFY_DB_MAXIDLE100Max idle connections in the pool.
PFY_DB_MAXOPEN1000Max open connections in the pool.
PFY_DB_MAXLIFETIME1hMax connection lifetime (Go duration, e.g. 30m, 1h).

Dev-user override (dev_user_override.*)

An authentication bypass that assigns a fixed identity to unauthenticated requests. It is structurally unshippable: it requires both env=developmentand the explicit enabled flag, and the Manager refuses to start if enabled is set in any non-development environment. When active it logs a loud, repeated startup warning.

VariableDefaultDevProdNotes
PFY_DEV_USER_OVERRIDE_ENABLEDfalseopt-in onlymust be offMaster switch. Setting it outside env=development is a fatal startup error.
PFY_DEV_USER_OVERRIDE_ID(empty)dev onlyFixed user ID assigned to unauthenticated requests.
PFY_DEV_USER_OVERRIDE_EMAIL(empty)dev onlyFixed email.
PFY_DEV_USER_OVERRIDE_NAME(empty)dev onlyFixed display name.
PFY_DEV_USER_OVERRIDE_USERNAME(empty)dev onlyFixed username.

Pocket ID integration (pocket_id.*)

The Manager's client to the Pocket ID identity provider (used to resolve/search users). See Pocket ID below for the full picture.

VariableDefaultDevProdNotes
PFY_POCKET_ID_HOSThttp://pocketid.localhostBase URL of the Pocket ID instance.
PFY_POCKET_ID_API_KEY(empty)may be emptyrequiredSent as X-API-Key to the Pocket ID admin API. Validation rejects an empty key in production. A secret — never commit it.

Cron / executor (cron.*)

VariableDefaultDevProdNotes
PFY_CRON_BACKEND_HEARTBEAT_TIMEOUT1mHow long a registered backend may go without a heartbeat before it is considered down (Go duration).
PFY_CRON_TRIGGER_CHECK_INTERVAL30sHow often the executor scans for triggers to fire (Go duration).
PFY_CRON_METRICS_PORT9090Prometheus metrics port for the executor; range 1–65535.

ESB request bounds (esb.*)

Bound the memory and time an Enterprise Service Bus request may consume — unbounded reads and a timeout-less client are a memory/goroutine DoS surface.

VariableDefaultDevProdNotes
PFY_ESB_MAX_REQUEST_BYTES10485760 (10 MiB)Inbound request body cap.
PFY_ESB_MAX_RESPONSE_BYTES10485760 (10 MiB)Upstream response body cap.
PFY_ESB_FORWARD_TIMEOUT30sOutbound request timeout (Go duration).

Pilets (pilets.*)

VariableDefaultDevProdNotes
PFY_PILETS_MAX_UPLOAD_BYTES52428800 (50 MiB)Maximum pilet upload size.

Logger (logger.*)

VariableDefaultDevProdNotes
PFY_LOGGER_LEVELinfoLog level (debug, info, warn, error, …).

Invitations & email (invite.*, smtp.*)

Delivery for the tenant-invite flow. When PFY_SMTP_HOST is empty, no mail is sent — the Manager falls back to logging the plaintext invite link (and the mutation returns the token so the UI can surface it).

VariableDefaultDevProdNotes
PFY_INVITE_BASE_URL(empty)optionalrecommendedPublic invite-link prefix the email/plaintext-fallback links point at — the proxy-fronted OIDC entry, e.g. https://portal.example.com/invite.
PFY_SMTP_HOST(empty)optionalrecommendedOutbound SMTP host. Empty ⇒ invite mail is not sent (plaintext-link fallback).
PFY_SMTP_PORT587SMTP port.
PFY_SMTP_FROM(empty)From address for invite mail.
PFY_SMTP_USERNAME(empty)SMTP auth username.
PFY_SMTP_PASSWORD(empty)SMTP auth password. A secret — keep it out of committed files.

Proxy

The proxy is a Caddy build with the productify plugin. Its settings live in the Caddyfile global block; a few are wired to the environment. Caddy resolves {$VAR} at load time and {env.VAR} at request time.

VariableDefaultDevProdNotes
PFY_MANAGER_TOKEN(none — required)requiredrequiredThe proxy's token in the productify block — the shared secret it sends to the Manager as X-Proxy-Auth, and the key it derives the tenant-cookie signing key from. Must equal the Manager's PFY_PROXY_SECRET. A secret.
PFY_OAUTH_CLIENT_SECRET(none — required)requiredrequiredOAuth client secret for the caddy-security ↔ Pocket ID (OIDC) handshake. A secret.
PFY_ADMIN_LISTEN127.0.0.1:2019keep loopbackinternal onlyAddress of Caddy's admin API (:2019/load), which the Manager's config renderer POSTs rendered proxy config to for a graceful reload. Never expose :2019 publicly — bind it to an internal interface reachable only by the Manager, and firewall/authenticate that hop.

Session lifetime / idle policy

The caddy-security authentication portal (pocketportal) sets the portal session lifetime in the global security block:

caddyfile
crypto default token lifetime 3600

token lifetime 3600 is an absolute portal-session lifetime of 3600 seconds (1 hour) — there is no separate idle timeout, so after an hour the user is re-challenged through Pocket ID regardless of activity. Tune this per environment (shorter for higher-sensitivity deployments). Pocket ID owns its own upstream session/idle policy independently; this token only bounds the proxy-portal session. Logout invalidates it immediately, ahead of expiry.

See Session policy & logout for the full policy and the standardized logout contract.

Pocket ID

Pocket ID is the platform's identity provider. It is configured in two places:

  1. Proxy → Pocket ID (OIDC login). caddy-security authenticates users against Pocket ID via OIDC using PFY_OAUTH_CLIENT_SECRET and the OIDC discovery/base URLs in the Caddyfile oauth identity provider generic block (base_auth_url / metadata_url, e.g. http://pocketid.localhost).
  2. Manager → Pocket ID (admin API). The Manager resolves/searches users via the Pocket ID admin API using PFY_POCKET_ID_HOST and PFY_POCKET_ID_API_KEY (sent as X-API-Key).

Pocket ID is read-only from the Manager

The Manager's Pocket ID client only reads (GET /api/users). It does not create Pocket ID accounts or manage sessions — Pocket ID owns identity and session lifecycle. See Session policy & logout.

Minimum secure production config

The bare minimum to run a hardened production deployment. The Manager enforces the starred (*) items at startup and refuses to boot without them; the rest are operational must-dos.

  • [ ] Proxy secret set on both sides. PFY_PROXY_SECRET (Manager) is set and equals the proxy's PFY_MANAGER_TOKEN. Without it the Manager fails closed. *
  • [ ] Dev-user override off. PFY_DEV_USER_OVERRIDE_ENABLED is unset/false (and PFY_ENV=production, which alone makes the override a fatal error if enabled). *
  • [ ] Non-default database password. PFY_DB_PASSWORD is set to a strong, non-postgres value. *
  • [ ] TLS to the database. PFY_DB_SSLMODE is require (or stronger — verify-ca / verify-full); disable is rejected in production. *
  • [ ] At-rest encryption key. PFY_ENCRYPTION_KEY is a valid base64 32-byte key, so endpoint/backend secrets are encrypted at rest. *
  • [ ] Pocket ID API key. PFY_POCKET_ID_API_KEY is set. *
  • [ ] PFY_ENV=production. Disables the /gql playground, the dev reverse proxy, and the override escape hatch. *
  • [ ] Admin API not public. PFY_ADMIN_LISTEN binds an internal interface only; :2019 is never reachable from the internet.
  • [ ] Session lifetime reviewed. token lifetime in the Caddyfile is tuned for the deployment's sensitivity.
  • [ ] Secrets are secrets. PFY_PROXY_SECRET, PFY_MANAGER_TOKEN, PFY_OAUTH_CLIENT_SECRET, PFY_ENCRYPTION_KEY, PFY_POCKET_ID_API_KEY, and PFY_DB_PASSWORD come from a secret store, never from committed files.

Keeping this page honest

A drift guard parses the Manager's config struct and fails if any environment variable it reads is missing from this page.

Run it locally (with the manager repo checked out beside docs):

bash
cd docs
node scripts/check-config-drift.mjs
# or point it explicitly:
node scripts/check-config-drift.mjs ../manager/internal/config/config.go

It derives every PFY_* variable from the fig struct tags (plus any os.Getenv reads) and checks each appears in this file. In CI the config-drift workflow checks out both repos and runs it on every change.