clients/dashboard is what your tenants’ users see when they log in. Catalog browsing, realtime chat, file uploads, ticket filing, profile management — all themed by the tenant’s brand (colours, logo, typography pulled from the Multitenancy module). It’s the surface your customers spend most of their time in, so it’s the one to invest UX care in first.
Tech stack
Same shape as the admin console — React 19 + Vite 7 + TypeScript 5.7 + TanStack Query + React Router + Tailwind + Radix + Lucide. Two additions:
| Used for | |
|---|---|
clients/dashboard/src/realtime/ | SignalR subscriptions (chat, notifications, presence) |
clients/dashboard/src/sse/ | Server-Sent Events feeds (live audit, build-status, etc.) |
The dashboard subscribes to more realtime than admin — chat is a continuous-conversation surface, so SignalR is on the hot path.
Page inventory
clients/dashboard/src/pages/├── auth/ Login, signup, 2FA, forgot/reset password├── catalog/ Product browsing, search, detail, image gallery├── chat/ Channels, DMs, threads, mentions, reactions, pinning├── files/ Upload, list, share, trash, restore├── identity/ Profile, sessions, 2FA setup, password change├── settings/ Theme preview, notification preferences├── system/ Health, audit (limited to caller's events)└── tickets/ File ticket, list, detail, commentsPlus shared chrome (sidebar, top bar, notification bell) in clients/dashboard/src/components/.
Per-tenant theming
When a tenant signs in, the dashboard fetches the tenant’s TenantTheme from GET /api/v1/tenants/{id}/theme and applies the palette + brand + typography via CSS custom properties. The result: a single React build, one app, but every tenant gets their own brand.
Full details: theming.
Chat — the realtime showcase
clients/dashboard/src/pages/chat/ is the densest page in either frontend. Slack-style channels with:
- Three channel kinds — Channel (named), Direct Message (1:1), Group DM (3+).
- Message threads with reply count badges.
- Emoji reactions (toggle on/off, one row per emoji + count).
- Pinned messages with a dedicated panel.
@mentionautocomplete viamention-picker.tsx(queries/usersAPI as you type).- Typing indicators throttled to 3 seconds.
- Cursor-paginated message history with infinite scroll.
- Real-time updates via SignalR — new messages, edits, deletes, reactions, channel reads.
- Full-text search across messages.
The chat page is the canonical example of how to build realtime UI against AppHub:
// clients/dashboard/src/pages/chat/chat-page.tsx (simplified)const realtime = useRealtime();
useEffect(() => { if (!realtime.connection) return;
const onMessage = (msg) => queryClient.setQueryData(...); const onEdit = (msg) => queryClient.setQueryData(...); const onDelete = (id) => queryClient.setQueryData(...);
realtime.connection.on('ChatMessageCreated', onMessage); realtime.connection.on('ChatMessageEdited', onEdit); realtime.connection.on('ChatMessageDeleted', onDelete);
return () => { realtime.connection.off('ChatMessageCreated', onMessage); realtime.connection.off('ChatMessageEdited', onEdit); realtime.connection.off('ChatMessageDeleted', onDelete); };}, [realtime.connection, queryClient]);useRealtime() returns the shared HubConnection from realtime-context.tsx. One connection, many subscriptions.
Catalog — search + product detail
A product browsing surface that renders the Catalog module’s data. Search, filter by brand + category, product detail with image gallery.
Files — presigned upload flow
clients/dashboard/src/pages/files/ implements the kit’s two-step upload:
- Browser calls
POST /api/v1/files/upload-urlwith metadata (file name, size, content type, category, owner type). - Backend returns a presigned PUT URL.
- Browser uploads bytes directly to MinIO / S3 via the presigned URL — no proxy through the API.
- Browser calls
POST /api/v1/files/{id}/finalizeto flip the file fromPendingUploadtoAvailable.
Tickets — file + track support requests
Filing tickets from the dashboard, with priority + (optional) assignee + comments thread.
Identity — profile + sessions + 2FA
Self-service for the signed-in user:
Settings — preferences + theme preview
Realtime architecture
clients/dashboard/src/realtime/realtime-context.tsx exposes a single shared HubConnection:
const realtime = useRealtime();
// Subscribe in a componentuseEffect(() => { if (!realtime.connection) return; const handler = (n: NotificationDto) => /* ... */; realtime.connection.on('NotificationCreated', handler); return () => realtime.connection.off('NotificationCreated', handler);}, [realtime.connection]);The context handles:
- Authentication via
accessTokenFactoryfrom the auth context. - Automatic reconnect via
withAutomaticReconnect(). - Connection state changes (connecting, connected, reconnecting, disconnected) surfaced as a state pill.
Local development
cd clients/dashboardnpm installnpm run dev # http://localhost:5174 with HMRThe dev server proxies API calls to the backend’s URL configured in clients/dashboard/.env.
When run via the kit’s Aspire AppHost, the dashboard wires automatically — fsh-api, fsh-admin, fsh-dashboard all come up with one command, and service discovery handles the URLs.
Related
- Admin console — operator-facing companion.
- Frontend architecture — shared patterns.
- Theming — per-tenant brand customisation.
- Chat module — the API the chat surface wraps.
- Files module — the API the upload flow wraps.