Single Worker API authority
Hono routes live behind one Worker and expose the product API under /api/*. That lets the app keep route middleware, auth checks, D1 access, and Workers AI bindings in one deployable surface.
Interactive system brief
A single-page field guide to how the React workspace, staff shell, anonymous survey flow, Cloudflare Worker, D1 data model, and portability plan fit together.
How to read this
Interactive map
UserHappy keeps the browser bundle broad, the API authority narrow, and customer data separated behind explicit tenant resolution.
Backend decisions
The backend documentation centers on three choices: Cloudflare is the fast default, D1 keeps the data model SQLite-compatible, and adapter boundaries keep future migration from touching route logic.
Hono routes live behind one Worker and expose the product API under /api/*. That lets the app keep route middleware, auth checks, D1 access, and Workers AI bindings in one deployable surface.
The browser bundle can be shared, but hostnames and route guards make the customer app, staff shell, preview workflow, and respondent survey path feel distinct.
The architecture favors a registry plus isolated tenant stores. Even when the current implementation evolves, the core principle is unchanged: never let customer ownership become an afterthought in shared queries.
D1 gives a low-friction Cloudflare start. If scale or customer requirements shift, SQLite-compatible stores such as libSQL/Turso keep the migration centered on adapters instead of SQL rewrites.
Frontend decisions
The frontend documentation explains how React stays understandable without reaching for a heavier global store before the app actually needs one.
App.tsx uses hostname and routes to choose the staff shell, customer workspace, public auth routes, and token-based survey route from one Vite-built bundle.
LiveTeamContextProvider publishes identity, organization, system-user state, staff role, and impersonation provenance because those values affect navigation and page access across the route tree.
Route-local UI state remains in useState, useMemo, and useEffect. Browser-backed hooks handle device state such as the selected customer connection.
Pages call focused data modules for pulses, team access, audience, and staff onboarding instead of assembling fetch headers and JSON error parsing in every view.
Security and authentication analysis
The app uses passwordless email links for identity proof, short browser verification pages to exchange those links for JWT sessions, and Worker-side role checks to keep customer, system, staff, and respondent flows separated.
/api/auth/magic-link creates a random token, stores only its SHA-256 hash, and emails the raw token.
/api/auth/verify hashes the link token, checks expiry, accepts pending invites, and signs a JWT.
The browser stores the JWT under uh-session and sends it as Authorization: Bearer.
authMiddleware verifies the JWT, then role and org lookups decide what the request may do.
Auth Flow
Login requests return { ok: true } even when no active user exists, so the public form does not disclose which emails are registered. For active users, the Worker deletes prior unexpired auth tokens, creates a new random token, stores the hash, and sends the raw token through the email adapter.
Auth Flow
If a staff user starts from the app hostname, verification detects the userhappy-team org and returns or redirects to staff.userhappy.raiteri.net/auth/verify. That keeps staff onboarding and customer workspace login from collapsing into one browser surface.
Auth Flow
After a successful verification or impersonation exchange, AuthVerify stores the session token and calls history.replaceState so the raw email-token URL is not left in the visible address bar.
Enforcement
The Worker allows configured app, staff, preview, and local origins to send Authorization headers. CORS reduces accidental browser exposure; it is not the permission boundary. Protected routes still require a valid JWT and server-side role checks.
Enforcement
The JWT payload contains userId, orgId, and optional controlledBy. The Worker then queries active users by both user and org, which prevents a signed token from being treated as enough without confirming current database state.
Enforcement
Member invites, invite revocation, role changes, and member revocation call requireOwner(). Non-owner users can still use the product workspace, but cannot assign roles or revoke access through the team API.
Enforcement
The staff impersonation route signs a one-hour JWT for the target customer and includes controlledBy. The frontend displays staff provenance from /api/team/me, so customer actions taken under staff control are not invisible in the UI.
Risk Register
The schema has auth_tokens.used_at and the UI says links can only be used once. The observed verify path checks token hash and expiry, then signs a JWT; a hardened path should also reject already-used tokens and mark successful tokens used in the same exchange.
Risk Register
JWTs live in localStorage, which is straightforward for a static app but exposes the session to injected script. Keep UI injection surfaces narrow, consider a strict CSP, and revisit HttpOnly cookie sessions if the app threat model grows.
Risk Register
The current Worker route requires the caller to belong to userhappy-team. If impersonation is intended to be admin or super-admin only, the route should also verify staff_role before issuing a controlled customer token.
Risk Register
Respondent links intentionally avoid accounts, so the invite token itself grants survey access. Treat those URLs as sensitive, keep response uniqueness enforced, and add explicit token expiry if survey access needs a stronger lifecycle than status alone.
Request flow
The survey URL contains the invite token. No user account is created and no dashboard navigation is shown.
The same Vite bundle loads quickly from the edge. The route renders only the survey experience for /survey/:token.
The Worker resolves the token, fetches pulse metadata and questions, and returns the minimum payload needed by the respondent UI.
If the survey records audio feedback, the Worker can use its AI binding for transcription before the final answer set is submitted.
The Worker writes the response and marks the invite as used. Database constraints protect against duplicate respondent submissions.
Role access
Frontend role gates are a usability layer. Backend route checks and D1 ownership rules remain the security boundary.
customer_owner
customer_admin
customer_user
staff user
admin
super_admin
invite token
Concept finder
Static delivery for the Vite bundle. Hostname and routes decide which shell appears after the browser loads.
View in mapThe API authority for auth, pulses, contacts, audience, survey responses, team access, staff actions, and AI suggestions.
Backend decisionsD1 keeps the early stack compact while preserving a SQLite-compatible path for future adapter swaps.
Backend decisionsPublishes current user, organization, system-user state, staff role, and impersonation context to routed views.
Frontend decisionsEmail links prove initial identity, then the verification endpoint exchanges the link token for a signed JWT session.
Security analysisThe browser stores the signed session locally and sends it to Worker APIs as an Authorization bearer token.
Security analysisControlled customer sessions carry staff provenance so the UI can show who is acting for the customer.
Role accessOptional AI binding for pulse suggestions and survey audio transcription without a separate service hop.
View in mapNo matching concepts. Try a shorter term.
Scale and portability
Keep costs low, keep latency close to users, and avoid infrastructure overhead while the product surface stabilizes.
Track D1 storage, reads, writes, Worker requests, and customer data size. The architecture is still comfortably inside the Cloudflare shape.
Migration should be driven by customer requirements, cost shape, or operational needs, not by fear of the current stack.
Workers can move toward Lambda, D1 toward SQLite-compatible stores, Workers AI toward Bedrock, and Pages toward S3 plus CloudFront.