mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
ba6ac9b5cc91c57b2a60c85331255056e883ed23
Registration is the one moment this portal learns a person's name. IAM answers the create with the row it wrote, whose `id` is the account's stable subject — the same value that arrives later as the OIDC `sub`. Read it in parseCreated, carry it out on SignupResponse, hand it to onCompleted, and let the page identify() with it just before the completion is counted. The steps already queued keep the anonymous id they happened under, which is what they were; everything from the completion on is the person. One identify row joins the two halves, and without it a visit and the account it produced are two records nothing connects. The subject and nothing else. The row also carries the address, and the package stays the flow rather than the thing that watches it. The unload send is labelled text/plain. A cross-origin POST is preflighted unless its content type is CORS-simple, and a preflight is a second round trip that a document being torn down does not get. The bytes are the same JSON; the ingest reads the body it is handed rather than dispatching on the header, so nothing server-side is relaxed to accept it. Two tests pin both halves, and each fails without its own change.
@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
92.1%
CSS
4.5%
JavaScript
2.6%
Dockerfile
0.6%
HTML
0.2%