UH UserHappy Architecture Docs

Interactive system brief

Readable architecture for the UserHappy product surface.

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

Start with the map, then drill into decisions.

Interactive map

One product, distinct runtime boundaries.

UserHappy keeps the browser bundle broad, the API authority narrow, and customer data separated behind explicit tenant resolution.

Internet
Edge
Data and services

Backend decisions

Edge API, isolated data, portable contracts.

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.

B1

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.

B2

Domain context shapes behavior

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.

B3

Tenant isolation is the database story

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.

B4

SQLite compatibility preserves options

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

Role-aware shell, narrow context, simple state.

The frontend documentation explains how React stays understandable without reaching for a heavier global store before the app actually needs one.

F1

One bundle, multiple shells

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.

F2

Context only for cross-route identity

LiveTeamContextProvider publishes identity, organization, system-user state, staff role, and impersonation provenance because those values affect navigation and page access across the route tree.

F3

Local hooks handle local concerns

Route-local UI state remains in useState, useMemo, and useEffect. Browser-backed hooks handle device state such as the selected customer connection.

F4

Typed fetch modules keep pages clean

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

Identity is proven once; authorization is re-proven on every API call.

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.

1 Email link

/api/auth/magic-link creates a random token, stores only its SHA-256 hash, and emails the raw token.

2 Verification exchange

/api/auth/verify hashes the link token, checks expiry, accepts pending invites, and signs a JWT.

3 Bearer session

The browser stores the JWT under uh-session and sends it as Authorization: Bearer.

4 Worker proof

authMiddleware verifies the JWT, then role and org lookups decide what the request may do.

Auth Flow

Magic links avoid password state and account enumeration.

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.

worker/routes/auth.ts worker/lib/token.ts

Auth Flow

Staff identity is routed back to the staff hostname.

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.

staffVerifyUrl() src/pages/AuthVerify.tsx

Auth Flow

The verification page scrubs link tokens from browser history.

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.

USERHAPPY_SESSION_STORAGE_KEY /auth/verify

Enforcement

CORS is constrained, but authorization lives in the Worker.

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.

worker/index.ts authMiddleware()

Enforcement

JWTs carry identity; D1 proves account state.

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.

worker/lib/auth.ts currentCustomerUser()

Enforcement

Customer team administration is owner-gated.

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.

worker/routes/team.ts customer_owner

Enforcement

Staff impersonation is visible and time boxed.

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.

/api/staff/impersonate controlledBy

Risk Register

Link single-use semantics should be made explicit server-side.

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.

auth_tokens.used_at /api/auth/verify

Risk Register

LocalStorage bearer sessions make XSS the major browser risk.

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.

uh-session src/data/teamAccess.ts

Risk Register

Staff impersonation needs an explicit role gate if only admins should use it.

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.

worker/routes/staff.ts staff_role

Risk Register

Survey invite tokens are public bearer credentials.

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.

worker/routes/responses.ts invites.token

Request flow

Anonymous pulse submission from link to D1 write.

The survey URL contains the invite token. No user account is created and no dashboard navigation is shown.

Role access

Filter capabilities by persona.

Frontend role gates are a usability layer. Backend route checks and D1 ownership rules remain the security boundary.

Customer owner

customer_owner

  • Dashboard, Create A Pulse, Manage Audience, View Reports, Team, and Help Requests
  • Invite teammates, assign roles, revoke access, resend pending invites
  • Create, launch, close, and review customer-owned pulses

Customer admin

customer_admin

  • Pulse, audience, report, dashboard, and help request workflows
  • Team page is visible but role-management controls are hidden
  • Cannot revoke teammates or assign elevated customer access

Customer member

customer_user

  • Core workspace access for repeated product work
  • Pulse and audience work under backend permission rules
  • No invite, role-change, or revoked-access controls

UserHappy staff

staff user

  • Staff-domain shell, inbox, and profile workflows
  • Explicit customer connection before acting in customer workspace
  • No admin-only or super-admin route exposure

UserHappy admin

admin

  • Staff capabilities plus admin-only app routes
  • System help inbox and approval workflow navigation
  • No super-admin dashboard access

Super admin

super_admin

  • Admin capabilities plus super-admin guarded routes
  • Can create controlled customer impersonation sessions
  • Impersonation provenance remains visible in the app shell

Anonymous respondent

invite token

  • Open the survey route and submit answers once
  • Optional audio transcription through the Worker
  • No dashboard, team access, reports, or customer APIs

Concept finder

Search the architecture vocabulary.

Cloudflare Pages

Static delivery for the Vite bundle. Hostname and routes decide which shell appears after the browser loads.

View in map

Hono Worker

The API authority for auth, pulses, contacts, audience, survey responses, team access, staff actions, and AI suggestions.

Backend decisions

D1 and tenant isolation

D1 keeps the early stack compact while preserving a SQLite-compatible path for future adapter swaps.

Backend decisions

LiveTeamContextProvider

Publishes current user, organization, system-user state, staff role, and impersonation context to routed views.

Frontend decisions

Magic-link authentication

Email links prove initial identity, then the verification endpoint exchanges the link token for a signed JWT session.

Security analysis

Bearer JWT session

The browser stores the signed session locally and sends it to Worker APIs as an Authorization bearer token.

Security analysis

Staff impersonation

Controlled customer sessions carry staff provenance so the UI can show who is acting for the customer.

Role access

Workers AI

Optional AI binding for pulse suggestions and survey audio transcription without a separate service hop.

View in map

Scale and portability

Cloudflare now, adapter swaps later.

Now to 50 orgs

Build on D1 and Workers

Keep costs low, keep latency close to users, and avoid infrastructure overhead while the product surface stabilizes.

50 to 300 orgs

Monitor, do not migrate early

Track D1 storage, reads, writes, Worker requests, and customer data size. The architecture is still comfortably inside the Cloudflare shape.

300+ orgs

Evaluate the business trigger

Migration should be driven by customer requirements, cost shape, or operational needs, not by fear of the current stack.

Migration path

Swap adapters, preserve contracts

Workers can move toward Lambda, D1 toward SQLite-compatible stores, Workers AI toward Bedrock, and Pages toward S3 plus CloudFront.