Skip to content

Zero-to-App Local Quickstart

Go from nothing to an app running against Productify locally — the whole platform in one command, an example app wired up, and a trigger firing on your machine.

Requires the pfy dev stack

This guide uses the pfy dev commands (up, seed, proxy, status, down). They ship in the CLI; the platform images they start are pulled from ghcr.io/productifyfw/*. You need Docker + Docker Compose and the pfy CLI on your PATH.

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 dance — that middle ground is where most integration bugs hide. The full-stack loop is the real thing. This quickstart walks both, in order.

1. Bring up the platform and seed it

bash
pfy dev up && pfy dev seed
  • 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. Add --profile minimal to start only Manager + database for the bare loop.
  • pfy dev seed idempotently bootstraps a project, a dev tenant, an application, and a machine user, then writes two generated files (see Generated files). Re-running it is safe — it creates nothing that already exists.

Check health any time:

bash
pfy dev status

Pointing the CLI at the local Manager

pfy dev seed talks to the Manager the stack just started. If the CLI can't find it, set PFY_MANAGER_URL=http://localhost:8080 (or pass --manager).

Generated files

pfy dev seed writes two files into your app repo and gitignores both — they hold dev-only, visibly-fake (dev--prefixed) values, never production credentials:

FilePurposeConsumed by
.env.productify.devMANAGER_URL, APPLICATION_ID, MACHINE_USER_TOKEN, and PROXY_SHARED_SECRET (when set)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 shim, the dev proxy, and your tests — one source of truth for local identity.

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 injecting the standard identity headers (X-User-*, X-Tenant-*, X-Auth-Type: oauth, and X-Proxy-Auth when a secret is set) from dev.productify.json. It binds loopback only 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"}'

This is the closest bare-loop fidelity to production: your backend sees the exact headers the real proxy sends.

5. Full-stack loop — real login through the proxy

Now the real thing. The full stack from step 1 already runs the production proxy and Pocket ID. Reach your app through the proxy's hostname (see the Caddyfile for the vhosts the dev stack serves, e.g. http://vue3-example.localhost) and you get a real Pocket ID login, real tenant selection, and the proxy-injected identity headers.

Create the Pocket ID dev user

pfy dev seed prints a reminder to create the Pocket ID dev user (dev@local.test) for the full-stack login. Creating a Pocket ID account is not something the CLI does — the Manager's Pocket ID integration is read-only (see Session policy). Create it 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 owns passkey/email-code onboarding.

Then sign in through the app's hostname in the browser and complete the OIDC flow. Signing out uses the standardized logout contractwindow.__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 and confirm:

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

6. Fire your first trigger

Triggers are evaluated by the Manager's executor (running in the stack, on PFY_CRON_TRIGGER_CHECK_INTERVAL, default every 30s). 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 invoke it locally 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

Where to go next