The four are not four applications. They are one application whose `module` is a
value read off the hostname, because the thing behind all four — the framework
DocType engine — already treats an app lane as a module tag over a registry. The
first DNS label picks the site; the registered domain picks the brand. Keeping
those two resolutions apart is why erp.zoo.ngo is an ordinary sentence and not a
special case.
The renderer is @hanzo/ui/framework (list / detail / form from DocType metadata
alone), so this repo holds no per-module code and adding a fifth product is one
entry in SITES plus a DNS record. Auth is the console's IAM PKCE path — the access
token is the only credential, and cloud derives the org from its membership claim,
so this app never chooses a tenant.
Proven locally against a real cloud binary serving /v1/framework: nine Playwright
cases sign in through the app's own button and photograph all four hosts at 390x844
and 1440x900, asserting real records are on screen and that nothing scrolls
sideways on a phone.
Found on the way: @hanzo/brand's getBrand() takes a HOST, not a brand id, so
getBrand('lux') silently returns the Hanzo brand — every white-label caller keying
it by id is quietly wrong.
Co-authored-by: Hanzo Dev <[email protected]>
Running the four sites locally
Nothing here is part of the app. It stands up the two things the app talks to —
a real cloud binary serving /v1/framework/*, and an IAM issuer — so the
shell can be driven end to end without touching production.
1. A local cloud
hanzoai/cloud builds in ~25s and can run one subsystem:
cd ~/work/hanzo/cloud
export PATH="$HOME/.cargo/bin:$PATH" TMPDIR=$HOME/.cache/tmp
(cd native/flags && cargo build --release)
go build -o /tmp/cloudbin ./cmd/cloud
CLOUD_IAM_ISSUER=http://127.0.0.1:9099 \
/tmp/cloudbin -listen 127.0.0.1:8099 -data-dir ./.data \
-enable framework \
-kms-master-key-ref "$(printf 'hanzo-local-test-key-32-bytes!!!' | base64 -w0)"
-enable framework mounts the DocType engine and the app lanes that register
their fixtures with it (cms, erp, help, kb, content) and nothing else.
2. A local IAM
node dev/iam-stub.mjs # http://127.0.0.1:9099
It serves the four endpoints the @hanzo/iam browser SDK calls, so the app runs
its real PKCE flow — there is no dev-only auth branch in the app itself. The
JWT it signs carries the orgs claim cloud reads to decide tenancy, and cloud
validates its signature against the JWKS this serves.
3. The app
cp .env.example .env.local # points at the two above
npm run dev # http://localhost:3100
Then open the four hosts. *.localhost resolves to loopback in every browser, so
local development exercises the real host→site rule rather than a query
parameter: