Skip to content
fullstackhero

Guide

Tenant dashboard

The end-user-facing React + Vite app at clients/dashboard — catalog, chat, files, tickets, profile, plus per-tenant theming and real-time SignalR + SSE feeds.

views 0 Last updated

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, comments

Plus 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.
  • @mention autocomplete via mention-picker.tsx (queries /users API 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:

  1. Browser calls POST /api/v1/files/upload-url with metadata (file name, size, content type, category, owner type).
  2. Backend returns a presigned PUT URL.
  3. Browser uploads bytes directly to MinIO / S3 via the presigned URL — no proxy through the API.
  4. Browser calls POST /api/v1/files/{id}/finalize to flip the file from PendingUpload to Available.

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 component
useEffect(() => {
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 accessTokenFactory from the auth context.
  • Automatic reconnect via withAutomaticReconnect().
  • Connection state changes (connecting, connected, reconnecting, disconnected) surfaced as a state pill.

Local development

Terminal window
cd clients/dashboard
npm install
npm run dev # http://localhost:5174 with HMR

The 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.