Skip to content

dev ​

pfy dev runs a local copy of the platform for development: a Manager, its database, the Pocket ID identity provider, and the proxy, all from an embedded Docker Compose file. It can also seed a project and application into that Manager, and run a small proxy that injects identity headers so you can develop against your app without a real login.

Synopsis ​

bash
pfy dev <subcommand> [flags]

Start with the quickstart

The end-to-end walkthrough of both the "bare loop" (dev proxy) and the full-stack loop is the Zero-to-App Local Quickstart.

Description ​

pfy dev owns the local platform lifecycle:

  • up / down / status: bring the embedded compose stack (database, a one-shot Manager migration job, Manager, Pocket ID, proxy) up and down. The compose file is written to .pfy/dev/compose.dev.yml and run under the fixed compose project name pfy-dev. The migration job runs manager -migrate from the Manager image, which applies the migrations compiled into that binary
  • seed: idempotently bootstrap identity and data (project, tenant, application, enablement, environment, machine user) and generate the env/config files the integration libraries read
  • proxy: a loopback-only, header-injecting dev proxy, the production header contract without Pocket ID (the "bare loop")

Two profiles exist: full (manager + db + Pocket ID + proxy, the default) and minimal (manager + db only, for the bare loop).

dev up ​

Write (if absent) and start the local platform compose stack.

bash
pfy dev up [--seed] [--profile full|minimal] [--version <override>] [--detach=false]
FlagDescription
--seedAfter the stack is up, wait for the Manager API and run dev seed
--profilefull (manager+db+Pocket ID+proxy, default) or minimal (manager+db only)
--versionOverride pinned platform image tags: a bare tag pins manager+proxy, or a list like manager=2.0.0,proxy=1.1.0,pocketid=v2.14.0,postgres=16.4
--detachRun the stack in the background (docker compose up -d; default: true)
bash
pfy dev up
Wrote .pfy/dev/compose.dev.yml
…
Starting the full stack...
Stack started. Run 'pfy dev status' to check health.

--seed runs exactly what pfy dev seed runs, after waiting for the Manager's API to answer GET /health on its published port. It takes no seed flags of its own: for custom slugs, a different --dev-user-email or -o json, run dev seed as a second command. It cannot be combined with --detach=false, which is refused up front, because a foreground docker compose up blocks until the stack stops and the seed would run against a stack that had already gone away. If the stack starts and seeding then fails, the stack is left running and the command exits non-zero, so you can fix the cause and retry pfy dev seed without rebuilding anything.

bash
pfy dev up --seed
[1/2] Starting the full stack...
[2/2] Stack started. Waiting for the Manager API before seeding...
Manager API is ready at http://localhost:8080.
Created project pfy-dev / tenant dev / application dev-app / enablement / environment dev / machine user dev-machine-user
Wrote .env.productify.dev and dev.productify.json
Stack is up and seeded. Run 'pfy dev status' to check health.

The two generated files land in the current directory, so run it from your app's repository root.

Docker (with the compose plugin) is required; a preflight check reports what is missing. The check also probes each host port the selected profile publishes, and is skipped entirely when your own pfy-dev stack is already running, because compose converges a running project rather than re-binding its ports. A port it may not bind because it is privileged (80 and 443 on Linux, where an unprivileged process cannot bind below 1024) is skipped rather than reported as a conflict: Docker publishes those from a root daemon, and a genuine conflict there surfaces from docker compose itself.

The image tags are pinned in the binary, not floating: pfy 2.0.0 starts ghcr.io/productifyfw/manager:2.0.0, ghcr.io/productifyfw/proxy:1.1.0, ghcr.io/pocket-id/pocket-id:v2.14.0, and postgres:16.4. Manager and proxy track the newest tag each repository publishes; Pocket ID and PostgreSQL track what the install bundle runs, so the dev loop develops against the same identity provider and the same database major the platform ships. --version overrides any of them for one run: a bare tag (--version 2.0.0) pins manager and proxy together, and a list (--version manager=2.0.0,proxy=1.1.0) pins them separately.

Manager 2.0.0 is a floor, not a preference: pfy 2.0.0 selects Deployment.applicationId on every deployment read, and that field does not exist before Manager 2.0.0, so an older pin makes pfy deployment list and pfy nomad render fail schema validation.

Pocket ID v2 is a different major from v1, not a newer build of it, and the generated compose file carries what the major requires: ENCRYPTION_KEY (mandatory in v2, minimum 16 characters), a /healthz health check, a STATIC_API_KEY that provisions an admin service account so the stack converges without a browser step, an APP_URL that carries the published port (http://pocketid.localhost:1411, because the issuer in the OIDC metadata must be the address a browser also reaches), and a pocketid.localhost network alias so the proxy can resolve that issuer while loading its config.

dev down ​

Stop the local platform stack.

bash
pfy dev down [--profile full|minimal] [--volumes]
FlagDescription
--volumesAlso delete the stack's data volumes (destroys the local database)

dev status ​

Report per-service health and seeded state of the local platform.

bash
pfy dev status
Service health (full stack):
  - manager      healthy
  - postgres     no HTTP probe (use 'docker compose ps')
  - pocketid     healthy
  - proxy        healthy
Seeded state:
  - .env.productify.dev present (run 'pfy dev seed' again to refresh)

dev seed ​

Idempotently bootstrap the local platform (project, tenant, application, the tenant's enablement for it, a dev environment and a machine user in that environment) and generate the two files the integration libraries and shims read (both are auto-gitignored):

  • .env.productify.dev: MANAGER_URL, APPLICATION_ID, MACHINE_USER_TOKEN, and PROXY_SHARED_SECRET (the running dev stack's secret, or --proxy-secret)
  • dev.productify.json: a fake window.__PRODUCTIFY__ payload with a dev users list
bash
pfy dev seed [--project <slug>] [--app <slug>] [--tenant <slug>] [--dev-user-email <email>] [--proxy-secret <secret>] [-o json]
FlagShortDescription
--projectProject slug to seed (defaults to project.yaml's project_name, then pfy-dev); the Manager caps a project slug at 10 characters
--appApplication slug to seed (default: dev-app)
--tenantTenant slug to seed (default: dev)
--dev-user-emailEmail of the local dev identity written into dev.productify.json (default: dev@local.test)
--proxy-secretShared secret to present as X-Proxy-Auth and record as PROXY_SHARED_SECRET (defaults to the running dev stack's PFY_PROXY_SECRET, read from .pfy/dev/compose.dev.yml)
--output-otext (default) or json, the created IDs for scripting (local flag)

Running it again is safe: anything already seeded is left alone and reported (Everything already seeded — nothing to create.). Both generated files are written with mode 0600 because .env.productify.dev contains MACHINE_USER_TOKEN. The command ends by reminding you to create the Pocket ID dev user for the full-stack login, and that the browser login is a separate Manager user.

How seeding authenticates ​

The dev stack's Manager sets PFY_PROXY_SECRET, so it rejects any request without a matching X-Proxy-Auth. pfy dev seed therefore reads .pfy/dev/compose.dev.yml for the Manager's published port and that secret, and presents the secret plus the X-Token-* identity headers, exactly as the proxy would. It does that only when the target host is loopback and the port is the one that file publishes, so a dev secret cannot leave the machine, and on that path the stored token from pfy login is never sent.

The target order is --manager, then PFY_MANAGER_URL, then the local dev stack, then ~/.pfy/config.yaml. A running dev stack deliberately outranks the stored config: a config left over from pfy login against a real deployment must not make pfy dev seed create projects there.

The seeded identity is a UUIDv5 of --dev-user-email, so every run resolves to the same Manager user. A browser OIDC login arrives with Pocket ID's own subject, which is a different Manager user and sees nothing this created until you add it to the project.

dev proxy ​

Run a loopback-only, header-injecting dev proxy in front of your app: the bare loop. It injects the same identity headers the production proxy sends, from the dev.productify.json identity file, with no OIDC round-trip. It performs no authentication and refuses to bind non-loopback addresses.

Before injecting, it deletes every proxy-managed identity header on the inbound request, the same set the production proxy strips: X-User-*, X-Tenant-*, X-Auth-Type, X-Proxy-Auth, and anything starting with X-Token-. A forged header sent by a client therefore never reaches the app.

bash
pfy dev proxy [--upstream localhost:8080] [--port 9000] [--bind 127.0.0.1] [--config dev.productify.json] \
  [--proxy-secret <secret>] [--pilet-bundle dist/index.js] [--pilet-name <name>] [--pilet-version <v>]
FlagDescription
--upstreamThe app to forward requests to, as host:port or a full URL (default: localhost:8080)
--portPort to listen on (default: 9000)
--bindLoopback address to bind (default: 127.0.0.1; non-loopback addresses are refused)
--configIdentity file (the dev.productify.json that pfy dev seed generates; default: dev.productify.json). Missing file: run 'pfy dev seed' first
--proxy-secretShared secret to inject as X-Proxy-Auth (omitted when unset)
--pilet-bundlePath to a locally built pilet bundle (e.g. dist/index.js) to serve through a signed local feed
--pilet-nameName for the local pilet feed item (defaults to a project.yaml-scoped name)
--pilet-versionVersion for the local pilet feed item (default: 0.0.0-local)
pfy dev proxy on http://127.0.0.1:9000 → localhost:8080
  WARNING: this proxy performs NO authentication — it injects a chosen dev identity. Loopback only.
  Switch identity: GET http://127.0.0.1:9000/_dev/users, POST http://127.0.0.1:9000/_dev/switch {"user":"..."|"tenant":"..."}

The /_dev/users and /_dev/switch endpoints let you flip between the identities in the identity file without restarting.

With --pilet-bundle, the proxy serves a signed Piral feed at /pilet-feed/local listing that one bundle, and adds two keys to the window.__PRODUCTIFY__ config it injects into HTML responses: pilet_feed (/pilet-feed/local, which points the frontend dev shim at the local feed) and pilet_signing_keys (the feed's signing key, so the loader accepts it).

Typical loops ​

bash
# Full-stack: platform + real login
pfy dev up --seed
pfy dev status

# Bare loop: just your app + injected headers
pfy dev proxy --upstream localhost:8080 --port 9000

# Serve a locally built pilet through the dev proxy
pfy pilet create dashboard && cd dashboard && npm install && npm run build
pfy dev proxy --pilet-bundle dist/index.js

See Also ​