mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
674b6bb2633035cce136f553be1db400c75fea15
The wallet and the phone ranked third and fourth on the login screen, above the email field, because they were entries in a strip and the strip led the page. Somebody arriving to sign up for a small business met two specialist ways in before the one they were going to use. They are still offered, they just stop outranking the field: Google and GitHub lead, the form follows, the wallet and the phone trail. PROVIDER_ORDER carries the whole arrangement now rather than the buttons' half of it, so the form has a named place in the one list and the column can be read in one line. That is why the form is a child of the component that draws the column — entries fall on both sides of it, and a component cannot place entries around a sibling it does not contain. The alternative was a second list on the page saying which entries lead and which trail, and two lists of one arrangement drift. The rule travels with the form and appears only when something sits above it, so an app with no providers configured no longer gets a rule separating its form from nothing. It keeps the 24px of air it had when the page's gap owned it. The form no longer waits on the two descriptor reads. It was safe to return nothing until IAM answered while the form was a sibling; as a child that would be a blank screen wherever IAM is slow. Signup and device approval draw the same column from the same list. Device approval had the form above the strip under a comment claiming that matched the login page, which stopped being true when the one-click entries moved to the front.
@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%