Skip to main content

Web App

The Workloom web app (apps/web) is a React SPA built with Vite that provides a management interface for the registry. It consumes auto-generated React Query hooks from @workloom/workloom-api and authenticates via OIDC.

Quick Start​

# Start the API server (port 4050)
pnpm api:start

# Start the web dev server (port 3050, proxies API)
pnpm web:start

The Vite dev server proxies /v1/* requests to localhost:4050, avoiding CORS issues during development.

Technology Stack​

LayerTechnology
FrameworkReact 19 with TypeScript
BuildVite 6 with @vitejs/plugin-react
RoutingReact Router v6
Data fetching@tanstack/react-query v5 (via @workloom/react-query)
API clientKubb-generated hooks (via @workloom/workloom-api)
AuthCustom AuthProvider + @supabase/supabase-js
Transport@workloom/fetch-client (native fetch wrapper)

Authentication Flow​

API-mediated Supabase OIDC — the web app signs the user in to Supabase, then the API brokers the OAuth exchange and issues a wl_ session token:

1. User signs in to Supabase (email/password) via supabase-js
2. GET /v1/auth/oidc/authorize -> Supabase authorize URL (PKCE)
3. Supabase consent at /oauth/consent (approve)
4. Supabase redirects to /callback?code=...
5. POST /v1/auth/oidc/callback -> API verifies ES256 id_token, issues wl_ token
6. Token stored in localStorage; AuthProvider syncs it via setAuthToken()
7. All API requests carry Authorization: Bearer wl_<token>

Key Auth Hooks​

HookPurpose
useAuthAuth context: token, isAuthenticated, login/logout
useAccessTokenCurrent wl_ token + { accessToken, headers }
useRolesFetches role from /v1/auth/me (React Query, 5-min stale)

Token Injection​

The WorkloomQueryProvider subscribes to OIDC auth state and calls setAuthToken() from @workloom/fetch-client whenever the token changes. This ensures all generated hook calls automatically include the Bearer token without each component managing auth manually.

Pages and Routing​

RoutePageDescription
/dashboardDashboardOrg health, activity, approvals
/registryRegistryPackage browser
/registry/:namePackageDetailPackage versions, assets, governance
/workspacesWorkspacesWorkspace listing
/workspaces/:idWorkspaceDetailBindings, packages, drift, audit
/editorEditorAsset editing interface
/telemetryTelemetryOperational dashboards
/adminAdminSSO, teams, RBAC, audit logs

Composition Hooks Pattern​

The web app uses composition hooks to transform raw API data into view models. Each hook calls multiple generated React Query hooks and composes the responses into a typed model shape.

// Example: useDashboardModel composes multiple API queries
function useDashboardModel(options: { headers: Record<string, string>; enabled: boolean }) {
const packages = useListPackages({ client: { headers }, query: { enabled } });
const revisions = useListWorkspaceRevisions({ client: { headers }, query: { enabled } });
const telemetry = useTelemetryDashboard({ client: { headers }, query: { enabled } });
const approvals = useListApprovals({ client: { headers }, query: { enabled } });

const status = getCombinedStatus([packages, revisions, telemetry, approvals]);

return {
status,
data: status === "success" ? buildDashboardModel(packages.data, revisions.data, ...) : null,
};
}

Implemented Composition Hooks​

HookComposesProduces
useDashboardModelpackages, revisions, telemetry, approvalsDashboardModel
useRegistryModelpackagesRegistryModel
useWorkspaceListModelworkspace revisionsWorkspaceListModel

Design Rationale​

Composition happens client-side rather than via BFF endpoints because:

  1. The logic is pure view-model transformation (no I/O beyond what hooks provide).
  2. Avoids a second API surface to maintain.
  3. Scales naturally — adding a new page means adding a hook, not a new endpoint.
  4. Loading and error states are handled uniformly via getCombinedStatus.

Production Build​

# Build static assets
pnpm --filter @workloom/web build

# Output: apps/web/dist/

The production build outputs static assets that can be served by the API server (via Hono's serveStatic middleware) or deployed to a CDN.