Development environments
Forge is source-available, so the first environment anybody gets is not a sandbox account — it is the whole product, on their own machine, in minutes. From there the path to production has five steps, and each one promises something specific. This page says what, and where the promise stops.
| Step | Where it runs | What it promises |
|---|---|---|
| 1. A bench | your machine, from the source | the reference shop and admin, with a real catalogue and a working login, on localhost — in minutes, with no cloud account |
| 2. More benches | the same machine | any number of benches side by side, one number moving all of a bench’s ports, and no bench able to take over another |
| 3. The bench, reachable | your LAN, your VPN, your tailnet, a DNS name | the same bench over HTTPS from a phone or another machine, with one command and without reconfiguring anything |
| 4. Staging | your infrastructure | a real instance — the images production will run, pinned by digest — where a new version lands first |
| 5. Production | your infrastructure | the same instance with your hostnames and your data; versions move only when you move them |
Steps 1–3 are development environments: disposable, seeded, never holding real customers. Steps 4–5 are instances: they run released images, migrate forward only, and keep their data.
1. A bench on your machine
Section titled “1. A bench on your machine”You need Node 24 or newer, pnpm 11 and Docker with the compose plugin. Then, in a clone of the repository:
pnpm install --frozen-lockfilepnpm demo # Postgres, Redis, the kernel, a seeded catalogue, the storefront and the checkout behind one edgepnpm admin:demo # the admin, against the same kernelThe shop answers on http://localhost:3033 and the admin on http://localhost:3031/login. The admin login is
the real one — a one-time code by e-mail — and on a bench no e-mail leaves the machine: the bench invites a
reserved address and the code is written to the kernel’s log, with the command to read it printed for you.
What it costs: about eight minutes from a fresh clone to a signed-in admin, measured on 2026-10-10 on a 32-core Linux workstation with Docker’s build cache warm. The first run on a machine also pulls ~0.66 GB of catalogue photographs and the base images; nothing is downloaded twice.
What it promises: the kernel, storefront, checkout and admin running as the same container images a release
builds, from the tree you checked out — not development servers that behave differently from what ships. The
catalogue is seeded through the command port like any other write. A bench that comes up refuses loudly rather
than half-working: a Node below the floor is refused at install, and a kernel on a database nobody migrated says
so on /health instead of answering ok.
Where it stops: the bench runs the reference implementation from source. It is not your instance — your pinned images, your apps, your theme — and nothing on it is meant to last: tear it down and bring it back whenever you like.
2. As many benches as you need
Section titled “2. As many benches as you need”Two branches, two people, five agents working in parallel: each bench is its own checkout, its own compose project and its own block of ports.
export COMPOSE_PROJECT_NAME=bench-b FORGE_BENCH_PORT_BLOCK=77pnpm demo && pnpm admin:demo # shop on :7733, admin on :7731FORGE_BENCH_PORT_BLOCK=NN moves every port of the bench — the shop, the admin, the kernel, Postgres, Redis,
telemetry, the HTTPS doors — to NNxx. A single port variable still wins over the block.
What it promises: a bench never adopts another. Two checkouts that would resolve to the same compose project
are refused before the first container starts, naming the checkout that owns it — and the teardown,
pnpm box:down, refuses the same way, so tearing down one bench cannot take the other with it.
3. The same bench, from any network
Section titled “3. The same bench, from any network”A bench is born on localhost. One command moves it — shop and admin together — to an address other devices
reach:
pnpm bench:promote 192.168.1.20 # a LAN or VPN address, or a DNS name that points at this machinepnpm bench:promote tailnet # this machine on its Tailscale tailnetpnpm bench:promote localhost # backOff localhost there is only HTTPS, on purpose: the shop and the admin set Secure cookies, and a browser
silently drops those on plain HTTP — the cart would empty and the login would bounce with nothing on screen
saying why. On an address or a name, a local Caddy terminates TLS with its own certificate authority (one
browser warning, or import its certificate on the device once), or with certificates you provide. On a
tailnet, tailscale serve does it with a real certificate.
What it promises: every address the bench depends on — the store’s host, the media URLs, the admin’s links — moves together, from one declaration. The promotion never configures your network: making the machine reachable is your gesture. Tailscale is executed only when you ask for the tailnet, and a port another service already holds there is refused, not taken over.
Where it stops: the TLS is there for the browser’s rules, not as a perimeter. The network you promote to is the trust boundary — a bench has no authentication in front of the shop and seeds a known operator. Do not promote it to an address the public internet reaches.
4. Staging
Section titled “4. Staging”Staging is not a different product. It is an instance: the instance template that comes with Forge — a
compose file, the configuration, your apps and decisions, and a forge.lock that pins the four images by
digest — on your infrastructure, with a managed Postgres of its own. What makes it staging is only the order
you use it in: a new forge.lock lands here first.
source ./env-source.shdocker compose run --rm kernel node dist/migrate.js # forward-only, idempotent, transactionaldocker compose up -dbash bin/verify-composition.sh # the image composes the apps your lock pinsWhat it promises: the same bytes production will run. A digest names exact bytes, so an upgrade rehearsed here is the upgrade production gets — the same images, the same migration, the same commands. And the composition check catches the one thing digests cannot: the right version baked from a list of apps nobody intended.
5. Production
Section titled “5. Production”The same template again, with your real hostnames, your secrets, your backups. Upgrading is editing one file and running the same three commands; rolling back the code is putting the previous digests back. There is no merge in it, ever — see Deploy & the fleet.
What it promises: nobody upgrades you. Data never moves between instances, and migrations only go forward, so the snapshot you take before a version bump is your undo.
Your instance on your laptop
Section titled “Your instance on your laptop”Steps 1–3 are tooling of the Forge repository: they bring up the reference implementation, built from the
source. The instance template brings a bench of its own for your instance — the images your forge.lock
pins, your apps, your configuration — on your laptop, next to the production compose and without touching it:
bash bench/up.sh # Postgres, Redis, a local mailbox, the kernel, the edge and your fronts, on loopback portsbash bench/down.sh # down, data kept · bash bench/down.sh -v drops itIt needs Docker with compose, jq and curl — no clone of the Forge repository and no Node on the machine.
Measured on 2026-10-10 with the images already pulled: about 20 seconds from nothing to a store answering
and an admin to sign into; the login code arrives in the bench’s own mailbox, and no e-mail leaves the machine —
unless you declare the mailbox away (FORGE_BENCH_MAILBOX=none and your FORGE_SMTP_*, the password never in
the checked-in file) to test real delivery: then the code goes to the inbox of the operator you declare.
What it promises: it is production’s compose file with an overlay, so the box you test is the box you
deploy — the same images, the same edge and its routing. Every front is declared, one by one: the image
your lock pins, a build of your own fork, a process you run yourself (another framework, your own admin), or
none at all. The bench proves only what it owns — the database, the mailbox, the kernel answering, the doors
routed — and says what it does not prove; it never assumes a front is ours. Several benches of one instance run
side by side, each copy under a project name of its own even when two copies share a directory name, moved by
the same FORGE_BENCH_PORT_BLOCK as above; a copy never adopts another copy’s bench. Your data arrives through a seed hook your instance declares; without one the bench is
born with its store and first operator and nothing else.
A box of more than one brand fits too: several tenants, each with its own admin door and credential, the
admin running as a multi-brand box runs in production; doors of your own (a totem) published like the bench’s;
services that need what your seed creates started after it; and a cache down -v keeps. None of it changes a
bench that declares none of it.
And from any network, like step 3: bash bench/promote.sh <host or IP | tailnet | localhost> is the same
gesture with the same three destinations. A LAN or VPN address gets HTTPS from the bench’s own edge, with a
local certificate authority it exports for your devices to trust (or certificates you bring); tailnet gets it
from tailscale serve, which the bench runs only when you name that destination and never over a door another
bench already holds there. A door of your own declared :https (a totem on a tablet) travels with the promotion;
one declared without it stays on this machine, and the bench says so. A front you run yourself is told the new
address rather than assumed to follow, and coming back releases the admin addresses the promotion claimed.
Where it stops: the bench’s data and secrets mean nothing anywhere else, and a promotion reaches only the
networks your machine is on — it is not a staging box (step 4). The template’s README.md carries the whole
recipe.