Skip to content

Zero-to-App Local Quickstart ​

From an empty machine to an app running against a local Productify platform: the platform starts with one command, an example app is wired to it, and a trigger fires on your machine. Use it the first time you develop against Productify, or whenever you need a throwaway local platform.

What you need

  • Docker with the Compose plugin (docker compose version works).
  • The pfy CLI on your PATH.
  • These host ports free: 5432, 8080, 8081 (Manager + database), plus 80, 443, 1411, 2112 for the full stack. pfy dev up checks them first and refuses to start if one is taken (for example a PostgreSQL service already listening on 5432). Ports it may not bind because they are privileged (80 and 443 on Linux) are skipped, not reported as conflicts; Docker publishes those from a root daemon.

pfy dev up writes its Compose file and a dev Caddyfile into .pfy/dev/ in the current directory (and gitignores them), then starts images from ghcr.io/productifyfw/*, ghcr.io/pocket-id/pocket-id and postgres. The image tags are pinned inside your pfy build (pfy 2.0.0 pins manager 2.0.0, proxy 1.1.0, Pocket ID v2.14.0 and postgres 16.4); override them with pfy dev up --version manager=<tag>,proxy=<tag>,pocketid=<tag>,postgres=<tag>.

The two dev loops ​

Productify supports two local development loops. Know which you are in:

LoopWhat runsIdentityUse it for
BareJust your app + the tiny pfy dev proxy (or the @productifyfw/dev Vite shim)A fixed dev identity you choose, injected as the production header setPure domain/UI work: fast inner loop, no containers, no OIDC
Full-stackThe whole platform (Manager, database, Pocket ID, real proxy)Real login through Pocket IDAuth-adjacent work, tenant switching, end-to-end behavior

The bare loop gives you the exact production header contract without the four containers or the OIDC login flow. Most integration bugs show up there. The full-stack loop is the real thing. Both are walked below, in order.

1. Bring up the platform and seed it ​

bash
pfy dev up --seed

That is exactly pfy dev up && pfy dev seed, with a wait for the Manager's API in between. Run the two separately when you want to pass seed options: --seed takes none of its own.

  • pfy dev up writes (if absent) and starts the pinned-version Compose stack (Manager + PostgreSQL + Pocket ID + proxy) under the Docker Compose project pfy-dev. It detaches by default; pass --detach=false to keep it in the foreground, which cannot be combined with --seed. Add --profile minimal to start only Manager + database for the bare loop.
  • If the stack starts and seeding fails, the stack stays up and the command exits non-zero. Fix the cause and run pfy dev seed on its own.
  • Seeding creates a project, a dev tenant, an application, the tenant's enablement for that application, a dev environment and a machine user in it, then writes two generated files (see Generated files). Re-running it is safe; it creates nothing that already exists. Defaults: project pfy-dev (or project.yaml's project_name, capped at 10 characters), app dev-app, tenant dev.

Check health any time:

bash
pfy dev status

Which Manager seeding talks to

pfy dev seed finds the running dev stack on its own: it reads .pfy/dev/compose.dev.yml for the Manager's published port and the stack's PFY_PROXY_SECRET, and authenticates as that stack's trusted proxy. The order is --manager, then PFY_MANAGER_URL, then the local dev stack, then ~/.pfy/config.yaml. A running dev stack outranks the stored config so a pfy login against a real deployment cannot make seeding create projects there, and a token stored for another Manager is never sent to the dev stack.

Do not set PFY_MANAGER_URL for this. It is honoured verbatim, and pointing it at a remote Manager makes pfy dev seed write to that one with your stored token.

Generated files ​

pfy dev seed writes two files into the current directory and gitignores both. They hold dev-only values, never production credentials:

FilePurposeConsumed by
.env.productify.devMANAGER_URL, APPLICATION_ID, MACHINE_USER_TOKEN, and PROXY_SHARED_SECRET (the running stack's secret, or the one you pass to --proxy-secret). Written with mode 0600 because it carries the machine-user token.Your backend, pfy dev proxy
dev.productify.jsonA fake window.__PRODUCTIFY__ payload with a dev users listThe @productifyfw/dev Vite shim, pfy dev proxy

One dev.productify.json feeds the Vite shim, the dev proxy and your tests, so local identity has one source of truth.

2. Get the example app ​

Scaffold a new project, or clone an example to start from something that runs:

bash
# scaffold a fresh repo
pfy init

# or clone an example from the monorepo
git clone https://github.com/ProductifyFW/thesis
cd thesis/be-integrations/packages/node-express-example   # backend example
# (a Vue 3 frontend example lives under fe-integrations/)

Run pfy dev seed from your app's repo root so the generated files land beside its code.

3. Bare loop: the Vite shim ​

For pure frontend work, the @productifyfw/dev Vite plugin injects the same window.__PRODUCTIFY__ object the real proxy would, reading the dev.productify.json from step 1. No containers required.

ts
// vite.config.ts
import { defineConfig } from "vite";
import { productifyDevShim } from "@productifyfw/dev";

export default defineConfig({
  plugins: [
    productifyDevShim({ configFile: "dev.productify.json" }),
  ],
});
bash
npm run dev

Your SPA now sees a fully-populated window.__PRODUCTIFY__ (user, tenant, application, logout_url) exactly as in production. The shim hot-reloads when dev.productify.json changes and no-ops in production builds.

4. Bare loop: the header contract via pfy dev proxy ​

To exercise your backend against the real header contract without Pocket ID, put the header-injecting dev proxy in front of it:

bash
pfy dev proxy --upstream localhost:8080 --port 9000

It forwards to your app and injects the standard identity headers (X-User-ID, X-User-Email, X-User-Name, X-User-Username, X-Tenant-ID, X-Tenant-Name, X-Auth-Type: oauth, and X-Proxy-Auth when you pass --proxy-secret) from dev.productify.json. It binds loopback only (--bind refuses any other address) and performs no authentication; the startup banner says so.

Switch identity or tenant instantly, no cookies, no OIDC:

bash
curl http://127.0.0.1:9000/_dev/users
curl -X POST http://127.0.0.1:9000/_dev/switch -d '{"user":"dev@local.test"}'

Your backend now sees the same headers the real proxy sends, without Pocket ID.

5. Full-stack loop: real login through the proxy ​

Now the real thing. The full stack from step 1 runs the production proxy build and Pocket ID. The generated .pfy/dev/Caddyfile.dev serves two hostnames:

HostnameServes
http://pocketid.localhostPocket ID (admin UI and OIDC endpoints)
http://app.localhostThe Manager, behind the Pocket ID login portal

Your own app has no vhost in that file yet. Add one to .pfy/dev/Caddyfile.dev (the file is generated once and your edits survive pfy dev up), modelled on the per-application blocks in the Caddyfile reference, and restart the proxy with docker compose -p pfy-dev restart proxy. Reaching your app through that hostname gives you a real Pocket ID login and the proxy-injected identity headers.

*.localhost names resolve to 127.0.0.1 on most systems without an /etc/hosts entry.

Create the Pocket ID dev user ​

pfy dev seed ends with a reminder to create the Pocket ID dev user (dev@local.test) for the full-stack login. The CLI does not create Pocket ID accounts; the Manager's Pocket ID integration is read-only (see Session policy). Create the user once in the Pocket ID admin UI (http://pocketid.localhost), using the same email you passed to pfy dev seed (--dev-user-email, default dev@local.test). Pocket ID handles passkey and email-code onboarding.

Then sign in through the app's hostname in the browser and complete the OIDC flow.

That browser login is a different Manager user from the one pfy dev seed ran as. Seeding authenticates as the stack's trusted proxy under a dev identity derived from --dev-user-email; the OIDC login arrives with Pocket ID's own subject, so the Manager keys it to a separate user that is a member of nothing. It sees an empty project list until you add it to the seeded project (Manager UI, the project's Members page, or pfy once you are logged in). Signing out uses the standardized logout contract, window.__PRODUCTIFY__.logout_url, which clears both the portal session and the tenant cookie.

Driving the Manager from the CLI ​

To operate the Manager from the CLI in the full-stack loop, log in through the proxy's Manager hostname and confirm:

bash
pfy login --server http://app.localhost --web   # browser SSO (device flow); or --token / --token-stdin
pfy whoami                                      # confirms the manager URL and token work

--server must be the proxied entrypoint (http://app.localhost), not the Manager's own port.

6. Fire your first trigger ​

The Manager's executor evaluates triggers every PFY_CRON_TRIGGER_CHECK_INTERVAL (default 30s). The dev stack runs the Manager with PFY_RUN_MODE=both, so the executor is already running. Create a trigger for your seeded application in the Manager UI (or via GraphQL; see the API Reference), point it at an endpoint on your app, and watch the executor call it within the check interval. See Triggers & Automation for authoring triggers.

That is the full path: platform up, app wired, both loops exercised, a trigger firing on your machine.

Tear down ​

bash
pfy dev down            # stop the stack (keep data)
pfy dev down --volumes  # stop and destroy the local database

Troubleshooting ​

  • host port 5432 is already in use (or another port): pfy dev up checks every port the stack publishes before starting. Stop the other service or free the port; the message names it. Your own running pfy-dev stack never raises this: the check is skipped when it is up, so re-running pfy dev up converges it rather than reporting a conflict against itself.
  • The Manager API never became ready under --seed: the stack is left running and the message names the URL that did not answer. Check pfy dev status and the Manager's container logs, then run pfy dev seed once it is up.
  • Seeding cannot reach the Manager: it resolves the target from .pfy/dev/compose.dev.yml, so run it from the directory pfy dev up wrote that file into, or pass --manager. If PFY_MANAGER_URL is set in your shell it wins over the dev stack; unset it.
  • Check health: pfy dev status probes the Manager (:8081/healthz), Pocket ID (:1411/healthz) and the proxy (:2112/readyz) and reports whether the seeded files exist.

Where to go next ​