Hanzo Dev 05237b0470
Hanzo CI/CD / cicd (push) Successful in 1m21s
CI/CD / cicd (push) Successful in 1m21s
the image installs what the tests ran
hanzo.id has not published an image since the telemetry commit. The build
fails at `vite build` with

  Cannot find module '/build/apps/web/node_modules/vite/bin/vite.js'

which reads as a broken tree but is not: the same commit builds cleanly when
its dependencies come from pnpm-lock.yaml.

The image never used the lockfile. It was not copied, and the install ran
`--frozen-lockfile=false`, so the container re-resolved the entire graph from
the registry on every build while `pnpm test` on the runner installed from the
lockfile -- two different dependency graphs out of one commit, the tested one
and the shipped one. That agreed for as long as free resolution happened to
land on the same versions, which is not a property anyone was maintaining.
Adding @hanzo/event was enough to break the tie: the lockfile pins
vite@7.3.5_@types+node@25.9.3_..., free resolution chose
vite@7.3.6_@types+node@22.20.1, and the peer-suffixed store path the workspace
link pointed at no longer existed. The dependency did nothing wrong; it
disturbed a resolution that was never pinned.

So the lockfile ships and the install is frozen to it. The image now gets the
exact tree the tests passed against, and a stale lockfile fails at the install
naming the mismatch rather than quietly building a different application.

apps/account/package.json joins the pre-install copies: a frozen install
validates the lockfile against EVERY workspace member, and apps/account is in
the workspace even though this image does not build it.

Verified from a cold store on this exact commit: frozen install resolves the
pinned vite, `pnpm --filter @hanzo/id-web build` succeeds, and the ingest key
is present in apps/web/dist -- the assertion the next step makes.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:05:20 -07:00
2026-02-14 05:26:48 -08:00
2026-08-04 11:22:27 -07:00
2026-06-28 20:06:13 -07:00

id

@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

  1. Publish or workspace-link the new per-org brand pkg (must ship brand.json at the package root and match the BrandContract shape in pkgs/shared/src/types.ts).
  2. Add a DEFAULT_TENANTS entry in pkgs/shared/src/tenant.ts OR put the override in the runtime catalog (IAM_TENANT_CONFIG_JSON env) so no rebuild is needed.
  3. Add the hostname to apps/web/vite.config.ts::BRAND_PACKAGES (lets dev + build serve /brand/<pkg>/brand.json).
  4. Add the hostname + TLS secret to apps/web/k8s/ingress.yaml.
  5. 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.

S
Description
No description provided
Readme MIT
2 MiB
Languages
TypeScript 89.3%
JavaScript 5.3%
CSS 4.3%
Dockerfile 0.9%
HTML 0.1%