main
Second half of the migration, and deliberately a separate commit: the canonical lane was landed first and watched green while deploy.yml was still in place, so this repo was never left with zero delivery. Three files out, one line changed. .hanzo/workflows/deploy.yml — superseded. The canonical lane builds the same Dockerfile, reads the same package.json version, pushes ghcr.io/hanzoai/id, and proves the manifest resolves before going green. It also runs the 160 tests and the typecheck that this file never did. .github/workflows/docker.yml — a second image lane. It had already been reduced to an echo, but it kept the SHAPE of a build lane alive in a directory this forge cannot even read (Gitea takes the first of WORKFLOW_DIRS present, and .hanzo/workflows has existed here for months). Two lanes for one image is how the two drifted in the first place. .github/workflows/workflow-sanity.yml — imports hanzoai/.github/.github/workflows/workflow-sanity.yml@main, which returns 404: the reusable it calls does not exist. It could only ever have failed. It also triggers on `paths: ['.github/workflows/**']`, a directory that is now empty, so even a live version of it would be guarding nothing. sync-from-github.yml keeps its cron and its fast-forward-only rule, and its build dispatch is repointed deploy.yml -> cicd.yml. That reference is BY FILENAME. Left alone it would have 404'd against the `|| echo` that makes it non-fatal, so the symptom would not have been a red sync — it would have been commits arriving on the forge and nothing building, silently, which is the exact failure this repo already survived once (four commits, zero images, no red). Co-authored-by: Hanzo Dev <dev@hanzo.ai>
@hanzo/id
White-label login + identity verification portal. One Vite SPA, four hosts
(hanzo.id, lux.id, zoo.id, pars.id), per-tenant brand resolved from
the request hostname at runtime.
Layout
apps/
web/ Vite + React 19 + @hanzo/gui — the actual SPA
k8s/ Deployment + Service + Ingress (4 hosts, 4 TLS secrets)
pkgs/
shared/ @hanzo/id-shared — TenantConfig + brand resolver
auth/ @hanzo/id-auth — composable login/signup/OTP flows
on top of @hanzo/iam SDK
idv/ @hanzo/id-idv — pluggable identity verification
(Persona, Onfido, Veriff, stub)
legacy-nextjs/ Frozen — predecessor Next.js implementation. Kept
for reference until v0.1.0 ships to production.
Local dev
pnpm install
pnpm dev # http://localhost:5173 (defaults to hanzo brand)
To preview a different brand locally, edit /etc/hosts:
127.0.0.1 lux.id zoo.id pars.id
then visit http://lux.id:5173.
Build
pnpm build # builds apps/web -> dist/
docker build -t ghcr.io/hanzoai/id:0.1.0 .
Adding a brand
- Publish or workspace-link the new per-org brand pkg (must ship
brand.jsonat the package root and match theBrandContractshape inpkgs/shared/src/types.ts). - Add a
DEFAULT_TENANTSentry inpkgs/shared/src/tenant.tsOR put the override in the runtime catalog (IAM_TENANT_CONFIG_JSONenv) so no rebuild is needed. - Add the hostname to
apps/web/vite.config.ts::BRAND_PACKAGES(lets dev + build serve/brand/<pkg>/brand.json). - Add the hostname + TLS secret to
apps/web/k8s/ingress.yaml. - DNS: CNAME or A record → cluster ingress IP.
That's it — no per-brand Worker, no per-brand image, no per-brand deployment. One binary, four brands.
Plugging an IDV provider
import { registerProvider, createPersonaProvider } from '@hanzo/id-idv'
registerProvider(createPersonaProvider({
templateId: import.meta.env.VITE_PERSONA_TEMPLATE_ID,
apiKey: import.meta.env.VITE_PERSONA_API_KEY,
environment: 'production',
}))
The portal stays unchanged — switching providers is a single registration call at boot.
Languages
TypeScript
89.7%
JavaScript
5.5%
CSS
4.4%
Dockerfile
0.2%
HTML
0.1%