Vite + React 19 + @hanzo/gui + @hanzo/iam + @hanzo/base, forked from hanzo-starter (identical provider stack, vite config, auth + deploy contract). - one org-scoped people(name,role,team,email,avatar,bio) Base collection - three views: directory grid (avatar cards + quick search) / person profile / edit - friendly light surface built entirely from @hanzo/gui primitives - PKCE sign-in via hanzo.id; useQuery/useMutation carry the IAM bearer token
Roster
A searchable team directory — a real, buildable Hanzo app you fork on hanzo.app and deploy live on Hanzo Cloud.
Browse teammates as avatar cards, filter by team, search across every field, and open anyone for their role, contact, and bio. Add and edit people inline — each one is an org-scoped row in Hanzo Base.
- UI —
@hanzo/gui(the Hanzo design system) under Vite + React 19. A clean, friendly light surface built entirely from gui primitives — no second kit, no Tailwind. - Auth —
@hanzo/iam, OAuth2 PKCE against hanzo.id. No local passwords — IAM owns every credential interaction. - Data —
@hanzo/base, the IAM-native, org-scoped data plane.peopleis a real Base collection.
Three views: the directory grid, a person profile, and the edit form (add · update · delete).
Stack (pinned)
| Package | Version |
|---|---|
react / react-dom |
^19.2.4 |
@hanzo/gui + @hanzogui/config |
7.3.0 |
@hanzo/iam |
^0.13.1 |
@hanzo/base |
^0.2.1 |
vite |
^6 (@vitejs/plugin-react) |
typescript |
5.9.3 |
Run it
npm install
npm run dev # http://localhost:5173
npm run build # tsc --noEmit && vite build -> dist/
npm run preview # serve the production build (SPA fallback on)
Out of the box it runs against live Hanzo (hanzo.id + api.hanzo.ai) — no
config needed to see the sign-in flow. Copy .env.example to .env to point at
a different environment.
Environment contract
Only VITE_-prefixed vars reach the browser (this is a static SPA — there is no
server). Defaults in parentheses.
| Var | Purpose |
|---|---|
VITE_HANZO_IAM_URL (https://hanzo.id) |
OIDC issuer. |
VITE_HANZO_CLIENT_ID (hanzo-app) |
IAM application (<org>-<app>). Its redirect-URI list must allow this deploy's /auth/callback — see Ambient IAM. |
VITE_HANZO_REDIRECT_URI (${origin}/auth/callback) |
PKCE redirect. |
VITE_HANZO_BASE_URL (https://api.hanzo.ai) |
Browser-reachable Hanzo Base data plane. Deploy injects the provisioned URL. |
VITE_HANZO_API_URL / VITE_HANZO_API_KEY |
Optional — only if the app calls the Hanzo LLM gateway. Unused here; never commit a real key. |
How auth works — ambient IAM
login() starts an OAuth2 PKCE S256 redirect to hanzo.id; hanzo.id returns
to /auth/callback, where handleCallback() exchanges the code for tokens
(stored in localStorage, refresh-aware via offline_access). Every deployed
app is a static site at <slug>.hanzo.app; there is no server token — the
SPA authenticates the user in the browser and carries the resulting IAM JWT to
Base. "Ambient" means the app just reads the signed-in user via that token.
The one deploy requirement: the IAM client (VITE_HANZO_CLIENT_ID) must list
this origin's /auth/callback as an allowed redirect URI. Register a
https://*.hanzo.app/auth/callback wildcard on the shared client so every forked
app works, or register a dedicated hanzo-team-roster client to scope it.
How data works — Base from schema.sql
schema.sql is the app's databaseSchema (SQL DDL). On publish,
Hanzo Cloud translates the people CREATE TABLE into a Hanzo Base collection
(provisionBaseFromDDL, additive + idempotent). Base manages
id/created/updated/owner/org, stamps owner+org from the verified
IAM principal, and scopes every row to the caller's org — a teammate in your org
sees the row; other orgs cannot. At runtime src/views/directory.tsx,
profile.tsx, and edit.tsx read/write that collection through
@hanzo/base/react (useQuery/useMutation) carrying the IAM token.
Deploy — Hanzo Cloud
hanzo.yml declares a static build (npm run build → dist/,
served at <slug>.hanzo.app) plus the Base schema to provision and the env to
inject. Do not build a container image locally — Hanzo Cloud owns builds and
deploys. CI here only proves the template compiles green.
Layout
src/
main.tsx entry
providers.tsx GuiProvider -> IamProvider -> BaseProvider(client=IAM-token)
app.tsx route (/auth/callback) + auth gate (light Theme)
gui.config.ts createGui(defaultConfig from @hanzogui/config/v5)
iam.config.ts IAM PKCE config
env.ts the VITE_ env contract, one place
lib/base.ts BaseClient carrying the IAM bearer token
lib/roster.ts Person type + directory helpers + the light UI palette
auth/callback.tsx PKCE return leg
views/
signed-out.tsx landing with an illustrative directory preview
home.tsx signed-in shell — one Route (list · profile · edit)
directory.tsx the searchable avatar-card grid
profile.tsx a person's profile
edit.tsx add / edit / delete a person
kit.tsx the light-surface atoms (avatar · card · inputs · buttons)
brand.tsx the Roster mark
schema.sql databaseSchema -> the `people` Base collection on publish
hanzo.yml Hanzo Cloud build/deploy manifest