Productify Proxy
The Productify Proxy is the front door of a Productify installation. It is a standard Caddy 2.11 web server compiled with two extra module sets: caddy-security for OAuth2/OIDC login, and the productify modules from the proxy repository. The productify modules validate credentials against the Productify Manager, work out which tenant a request is for, add identity headers before the request reaches your application, and record per-application access logs and metrics.
You normally do not write the Caddyfile by hand
The Manager renders one virtual-host block per application domain from its own data and pushes the whole Caddyfile to the running proxy through Caddy's admin API (POST :2019/load). The install-level part of the file (global options, the login portal, the Manager's own vhost) is rendered from install.yaml by pfy platform render and handed to the Manager as proxy_prelude_file. The Caddyfile examples in this section show the shape the renderer produces, so you can read and debug it. See Generated Artifacts and the Proxy Vhost Sync Loop. pfy proxy status, diff and sync inspect and drive that loop (CLI reference).
What the proxy does with a request
For a request to an application domain, the proxy runs these steps in this order:
productify_sanitizedeletes everyX-User-*,X-Tenant-*,X-Auth-Type,X-Token-*andX-Proxy-Authheader from the incoming request. Only the proxy may set them.- The caller is authenticated in one of three ways (table below).
- The tenant is resolved from the signed per-application cookie on a shared host, or from the
tenant <uuid>pin on a host dedicated to one tenant. - Identity headers (
X-User-ID,X-Tenant-ID, ...) are added and, for HTML responses, awindow.__PRODUCTIFY__object. - One access-log line and Prometheus metrics are recorded for the application.
- The request is forwarded to the application's upstream.
The compound directive productify_protect <app_id> with <policy> emits steps 1 to 5 in this fixed order; the rendered Caddyfile uses it for every application route. Application Routing explains each step.
Three ways to authenticate
| Caller | Credential | Routes | Manager unreachable |
|---|---|---|---|
| Person in a browser | OAuth2/OIDC login through the caddy-security portal under /auth/* | Application routes | The request continues with only the portal's X-Token-* headers; no X-User-* headers are added (see below) |
| API client, personal token | Authorization: Bearer <PAT> | Application routes | 503 Authentication service unavailable (fails closed) |
| Machine user (ESB) | Authorization: Bearer <token> or Authorization: Basic <base64> | /esb/* only | 503 Authentication service unavailable (fails closed) |
Other outcomes:
- An unauthenticated browser request is redirected (
302) to/auth/oauth2/<realm>; the portal then redirects to the identity provider. - A PAT the Manager reports as invalid falls through to the OAuth redirect. A valid PAT whose owner has no access to this application gets
403 Forbidden: no access to this application. - A browser user the Manager knows but does not authorise for this application gets
403 Forbidden: not authorized for this application. - On
/esb/*, a missingAuthorizationheader is401; a credential that is valid but not a machine user is403 ESB access requires Machine User authentication. Personal tokens and OAuth sessions are therefore rejected on ESB routes. - Basic credentials on an application route are ignored by the PAT step and fall through to the OAuth redirect.
Architecture
┌──────────────┐
│ Clients │ browsers, API clients, ESB systems
└──────┬───────┘
│ HTTP(S) :80 / :443
▼
┌─────────────────────────────────────────────┐
│ Productify Proxy (Caddy) │
│ caddy-security – OAuth2/OIDC portal │
│ productify_* – credential validation, │
│ tenant, headers, metrics │
│ :2019 admin API – receives rendered config │
│ :2112 – /metrics /healthz /readyz│
└──────┬──────────────────────────┬───────────┘
│ X-Proxy-Auth │
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ Productify Manager │ │ Application upstreams │
│ validate-proxy-auth │ │ (one vhost per app │
│ validate-oauth-user │ │ domain) │
│ frontend-config │ └───────────────────────┘
│ tenant catalog │
│ wake / ready │
└───────────────────────┘Modules in the custom build
| Name | Kind | Purpose |
|---|---|---|
productify (global block) | Caddy app | Manager URL, shared secret, cache TTLs, metrics address, activator defaults |
productify_protect | Route directive | Expands to the full protection chain below, in a fixed order |
productify_sanitize | Route directive | Deletes client-supplied identity headers |
productify_auth | Route directive | Validates a personal access token (Bearer) with the Manager |
productify_esb_auth | Route directive | Validates machine-user credentials (Bearer or Basic) with the Manager; ESB routes only |
productify_before_auth | Route directive | Counts users entering the login flow (pfy_authentication_awaiting_users) |
productify_tenant_selector | Route directive | Serves /_tenant, manages the signed tenant cookie, enforces the tenant pin |
productify_oauth_validation | Route directive | Maps a portal session (X-Token-Subject) to a Productify user and tenant via the Manager |
productify_html_inject | Route directive | Adds window.__PRODUCTIFY__ to HTML responses |
productify (route directive) | Route directive | Access log line and Prometheus metrics for one application |
activator (sub-block of protect) | Handler | Scale-from-zero: wakes a stopped deployment through the Manager and holds or parks the request |
caddy-security | Third-party | OAuth2/OIDC portal and authorization policies |
caddy-dns/cloudflare | Third-party | DNS-01 challenge so wildcard certificates (*.apps.example.com) can be issued |
Run caddy list-modules | grep productify on the built binary to confirm the modules are present.
How the proxy talks to the Manager
{
productify {
manager http://172.17.0.1:8080
token {env.PFY_MANAGER_TOKEN}
}
}token is the shared proxy secret. The proxy sends it in the X-Proxy-Auth request header on every call to the Manager API (not as an Authorization bearer token). The Manager compares it in constant time before it trusts any identity the proxy forwards. Set the same value on the Manager as PFY_PROXY_SECRET. Read it from the environment as shown; the proxy expands {env.VAR} when it starts.
The proxy refuses to start when token is empty or manager is not an http:// or https:// URL with a host. It also refuses a config whose min_proxy_version is newer than the plugin version it implements (currently 1.1.0), so a Manager that renders for a newer proxy fails loudly instead of misrouting.
Example: one application vhost
This is the shape the Manager renders for an application (the global block is on the Caddyfile page). vars app_id is the application's ID in the Manager.
http://app.example.com {
vars app_id 64e163af-2f6b-4b2c-9a84-ec3c8dfbbe67
@auth path /auth/*
@esb path /esb/*
# Login portal. Counts users waiting on the login page.
route @auth {
productify_sanitize
productify_before_auth with {vars.app_id}
authenticate with pocketportal
}
# ESB: machine users only. /esb/x becomes /esb/<app_id>/x on the Manager.
route @esb {
productify_sanitize
productify_esb_auth with {vars.app_id}
productify with {vars.app_id}
uri strip_prefix /esb
reverse_proxy manager:8080 {
rewrite /esb/{vars.app_id}{uri}
}
}
# Everything else: PAT or browser login, tenant, headers, metrics.
route /* {
productify_protect {vars.app_id} with pocketpolicy
reverse_proxy backend:8080
}
}The rendered file also adds a request_header -X-... snippet on every route as a second, independent strip of identity headers, and further routes for pilet feeds, pilet bundles, language packs and comms. Application Routing lists them.
Requirements
- The custom Caddy build: the published image
ghcr.io/productifyfw/proxy:1.1.0(Caddy 2.11.4, public), or a build from theproxyrepository'sDockerfileorgo buildincaddy/(Go 1.26). - A running Productify Manager, reachable from the proxy, with
PFY_PROXY_SECRETset to the proxy'stoken. - An OAuth2/OIDC identity provider (the repository's
docker-compose.ymlships Pocket ID). - DNS for every application domain pointing at the proxy.
Next steps
- Quick Start: run the proxy locally with one protected application.
- Caddyfile Reference: every global option and directive.
- Application Routing: routes, tenant resolution, headers your application receives.
- Monitoring: access logs, metrics, health probes.
- Proxy Deployment: production deployment.