Skip to main content

INFORMATION_ARCHITECTURE.md β€” h.work

Canonical IA reference. Maps every screen, route, and navigation pattern to the role that uses it. Cross-reference: USER_STORIES.md, GLOSSARY.md, FRONTEND_ARCHITECTURE.md, VIEWS_BY_ROLE.md, USER_FLOWS.md. Last updated: 2026-05-04.


0. What this platform is​

h.work is Humanity Protocol's internal operations platform for delivering managed AI+Expert services to client companies. The bulk of the application surface is internal: it's used by HP staff (Experts, Account Managers, Superadmins) the way an Intercom or Zendesk-style tool would be used inside any operations company.

There is also a client portal, but it is intentionally minimal: a chat shell so a client (e.g. Amy at Acme Financial) can message her assigned Specialist persona (Bob), plus a small admin section for Team / Channels / Billing. Clients don't run the platform β€” they receive a service through it. The Specialist persona is operated for them by Humanity Protocol's Expert team backed by AI agents.

This file documents the IA of all four surfaces and the routing rules between them.


1. Roles at a glance​

RoleWhoDB keyURL prefixHome routeNavigation pattern
Client (org employees: owner, admin, member, billing)Employees at the client companyorg_* client role/client/*/client/chatPersistent left sidebar inside /client/chat; nested /client/settings/* left sidebar (Profile / Team / Channels / Billing) for owner+admin
ExpertHumanity Protocol staff who review AI drafts and reply as the Specialistexpert platform role/workspace/*/workspace/queueTop tab bar (Queue / Tasks / Analytics / Settings)
Account ManagerHumanity Protocol staff who onboard and manage client orgsaccount_manager/ops/*/ops/clients48px top nav (Clients Β· Inbox Β· Settings) β€” same shell as Experts
SuperadminHumanity Protocol operations / cross-org accesssuperadmin/ops/*/ops/clients48px top nav (Clients Β· Inbox Β· Queue Β· Pools Β· Analytics Β· Health Β· Billing Β· Team Β· Settings) β€” strict superset of AM; same shell as Experts; Queue tab carries a pending-count badge

Role redirect after login is governed by getRoleRedirect() in src/contexts/AuthContext.tsx.


2. Site map β€” Client (/client/*)​

The client portal is intentionally minimal. The primary surface is the chat with the Specialist. The settings section has four sections only β€” Profile, Team, Channels, Billing β€” because clients do not configure the Specialist's behavior, manage tickets from their own end-customers, or operate the platform. Service configuration (agent name, risk threshold, slug) is handled by the AM during onboarding.

/client
β”œβ”€β”€ /client β†’ 302 β†’ /client/chat
β”œβ”€β”€ /client/chat CLIENT HOME β€” Specialist chat shell
β”‚ └── /client/chat/[id] Open thread by id
β”œβ”€β”€ /client/inbox Per-user notification inbox (#1866, 2026-06-05) β€” replaces NotificationBell drawer for client shell
β”œβ”€β”€ /client/onboarding First-run welcome (post-invite)
└── /client/settings [Profile open to all roles; admin sub-pages owner/admin only]
β”œβ”€β”€ /client/settings/profile Personal settings (name, avatar, phone, title, bio)
β”œβ”€β”€ /client/settings/team Team members, roles, invites
β”œβ”€β”€ /client/settings/channels Connected channels (Slack, Email, WhatsApp, Telegram)
β”‚ β”œβ”€β”€ /client/settings/channels/new Connect a channel
β”‚ └── /client/settings/channels/[id] Channel detail / disconnect
└── /client/settings/billing Invoices (Lago), payment method

# Legacy redirects (kept for inbound links from invite emails / bookmarks)
/client/settings β†’ /client/settings/profile
/client/admin β†’ /client/admin/team
/client/admin/team β†’ /client/settings/team
/client/admin/billing β†’ /client/settings/billing
/client/admin/channels[/...] β†’ /client/settings/channels[/...]
/client/admin/integrations[/...] β†’ /client/settings/channels[/...]
/client/admin/tickets β†’ /client/chat
/client/admin/settings β†’ /client/settings/team

Per-route detail β€” Client​

RoutePurposePrimary goalKey dataKey actionsEntry fromExit to
/client/chatConversational shell with the SpecialistSend a request, watch it get doneThreads list (left), open thread, message composerNew Thread, Send, Rename thread, View Specialist profilelogin, all /client/settings/* "Back to Chat" linkThread detail on send
/client/chat/[id]Single thread viewContinue a conversationAll messages, status badge, channel badgeSend, scroll historySidebar thread click, deep linkStatus changes, resolved state
/client/settings/profilePersonal preferencesTweak personal configProfile, theme, notification togglesSave profile, change passwordUserFooter "Settings" linkβ€”
/client/onboardingFirst-run wizard (Phase 2 of onboarding)Confirm setup, set password, configure channelsOTP, profile fields, slug confirm, channel togglesSave each step, CompleteInvite email link/client/chat after Phase-2 completion
/client/settings/teamMembers & invitesAdd/remove team membersMembers table, invite form, role select (Member / Admin / Billing)Invite, change role, removeSettings sidebarβ€”
/client/settings/channelsConnected channelsAudit and manage how Bob reaches the teamSlack/Email/WhatsApp/Telegram tiles with healthConnect, disconnect, testSettings sidebarChannel detail
/client/settings/channels/newAdd a channelConnect Slack/Email/WhatsApp/TelegramChannel picker, OAuth flowConnectChannels listChannels list
/client/settings/channels/[id]Channel detailVerify config and run testsWebhook URL, whitelist, test resultsTest, edit whitelist, disconnectChannels listChannels list
/client/settings/billingInvoices & paymentReview per-Specialist invoicesUpcoming invoice (itemized by Specialist), invoice history, payment methodDownload invoice PDF, manage payment methodSettings sidebarLago-hosted portal

What the client portal does NOT include (and why):

  • No "All Tickets" inbox. Clients converse with the Specialist via threads in chat β€” there is no helpdesk-style ticket queue. The admin team page is for managing colleagues, not work items.
  • No Specialist-behavior settings (agent name, confidence threshold, escalation rules). These are operator-only and configured by the AM during onboarding.
  • No analytics dashboard. Tracked as a P2 gap (USER_STORIES.md C-V*) β€” when delivered, it will be a usage dashboard for the client, not an Expert-handling-rate dashboard.
  • No support inbox for the client's own customers. This is not a helpdesk product. Acme Financial has no end-customers using h.work.

3. Site map β€” Expert​

/workspace
β”œβ”€β”€ /workspace/queue EXPERT HOME β€” pending work
β”‚ └── /workspace/queue/[id] Ticket detail (split-pane)
β”œβ”€β”€ /workspace/tasks Agentic task plan review (multi-step)
β”œβ”€β”€ /workspace/analytics Personal analytics
└── /workspace/settings Personal expert profile

Per-route detail β€” Expert​

RoutePurposePrimary goalKey dataKey actionsEntry fromExit to
/workspace/queuePending queue triageClear the highest-risk item nextTicket list with risk + confidence + SLA, filters, SLA stripFilter (status / risk / org / assignee), sort, open ticket, assign-to-meloginTicket detail
/workspace/queue/[id]HITL review for one ticketAccept, edit, or write a response and send as SpecialistCustomer thread, AI draft + confidence bar, internal notes, audit trailAccept & Send, Edit, Write from scratch, Defer, Reassign, Resolve, Add internal noteQueue list, deep link, push notifBack to queue, next ticket
/workspace/tasksMulti-step agentic task plansApprove/reject task stepsTask list, per-step plan + parameters, confidenceApprove step, Reject with feedback, Re-runTop navTask detail
/workspace/analyticsPersonal analyticsTrack resolution time, queue depth, AI/Expert mixCharts: volume, resolve time, confidence histogramDate rangeTop navβ€”
/workspace/settingsPersonal expert profileSet domain tags, availability, notification prefsSkills, certs, on-call statusSaveUserFooterβ€”

4. Site map β€” Ops (/ops/*)​

AM and Superadmin share the same /ops/* surface. SA is a strict superset: same routes, more nav tabs, and visibility across all orgs (vs. only assigned orgs for AM, RLS-enforced).

/ops
β”œβ”€β”€ /ops/clients OPS HOME β€” clients (AM: assigned; SA: all)
β”‚ β”œβ”€β”€ /ops/clients/new 5-step new client wizard
β”‚ β”œβ”€β”€ /ops/clients/[id] Client detail (members, channels, specialists, plan, activity)
β”‚ β”œβ”€β”€ /ops/clients/[id]/edit Edit client metadata
β”‚ └── /ops/clients/[id]/setup/ Onboarding wizard steps
β”‚ β”œβ”€β”€ step2 Specialist assignment
β”‚ β”œβ”€β”€ step-3 AM profile
β”‚ └── step-4 Send invite (Phase-0 gate)
β”œβ”€β”€ /ops/inbox Personal inbox (AM + SA)
β”œβ”€β”€ /ops/queue [SA only] Global expert queue (cross-org)
β”œβ”€β”€ /ops/analytics [SA only] Platform metrics
β”œβ”€β”€ /ops/health [SA only] Service health dashboard
β”œβ”€β”€ /ops/billing [SA only] All-org invoicing (Lago)
β”œβ”€β”€ /ops/team [SA only] Internal team (AMs, experts, AI operators)
└── /ops/settings Personal settings (AM + SA)

# Legacy redirects (kept for inbound links from bookmarks / docs)
/am/orgs β†’ /ops/clients
/am/orgs/new β†’ /ops/clients/new
/am/orgs/[id][/...] β†’ /ops/clients/[id][/...]
/am/settings β†’ /ops/settings
/superadmin/orgs β†’ /ops/clients
/superadmin/orgs/new β†’ /ops/clients/new
/superadmin/orgs/[id] β†’ /ops/clients/[id]
/superadmin/queue β†’ /ops/queue
/superadmin/analytics β†’ /ops/analytics
/superadmin/health β†’ /ops/health
/superadmin/billing β†’ /ops/billing
/superadmin/team β†’ /ops/team
/superadmin/settings β†’ /ops/settings

Per-route detail β€” Ops​

RouteRolesPurposePrimary goalKey dataKey actionsEntry fromExit to
/ops/clientsAM, SAClients (assigned for AM; all for SA)Pick the client to work onClient list (name, slug, status, trial end, last activity); SA sees extra columns: primary AM, MRR, last incidentNew client, Open, Search; SA: Impersonate, Suspend, ArchiveloginClient detail or wizard
/ops/clients/newAM, SAWizard step 1Capture company infoCompany info form (name, slug, industry, domains, country, timezone)Next β†’ step 2Client listStep 2
/ops/clients/[id]/setup/step2AM, SAWizard step 2Assign specialist(s)Specialist pool, primary toggle, thresholdAssign, Next β†’ step 3Step 1Step 3
/ops/clients/[id]/setup/step-3AM, SAWizard step 3Set AM profile (one-time)AM display name, avatar, signatureSave, Next β†’ step 4Step 2Step 4
/ops/clients/[id]/setup/step-4AM, SAWizard step 4Phase-0 gate + send invitePhase-0 checklist, invite previewSend inviteStep 3Client detail

Client-side onboarding wizard (post-invite) runs under /client/onboarding/* and includes an Agreements step (2.3a) β€” ToS / Privacy / AUP / DPA accept gates writing to the legal acceptance ledger; see docs/features/onboarding.md and #1046 / #1048 / #1068. | /ops/clients/[id] | AM, SA | Client detail | Manage one client | Tabs: Overview / Conversations / Specialists / Team. SA additionally sees: action bar (Impersonate, Reset password, Change plan β€” all "coming soon" stubs), inline org-role select per member with "Deactivate" stub, Tools tab (API only β€” no UI), Audit tab (placeholder). | Add member, change specialist, extend trial; SA: change member role | Client list | Edit / Wizard / member detail | | /ops/clients/[id]/edit | AM, SA | Edit client | Correct metadata | Form: name, contact, country, timezone | Save | Client detail | Client detail | | /ops/inbox | AM, SA | Personal inbox | Triage messages addressed to the operator | Conversation list | Open | Top nav | Conversation detail | | /ops/queue | SA only | Global queue | Spot systemic surges | All pending tickets across orgs, filters | Filter org / risk / status, drill-in | Top nav | Ticket detail (in workspace context) | | /ops/analytics | SA only | Platform analytics | Board-quality numbers | Volume, auto-resolve rate, top request types, active experts | Date range | Top nav | β€” | | /ops/health | SA only | Health dashboard | Detect incidents | Service status (API, DB, Redis, channel adapters) | Drill into a service | Top nav | β€” | | /ops/billing | SA only | All-org invoicing | Track revenue, oversee Lago invoices | MRR, upcoming invoices, invoice history per org, assignment/rate history | Drill into org, regenerate draft invoice | Top nav | Lago | | /ops/team | SA only | Internal team | Manage AMs/experts | User list with platform role | Invite, change role, deactivate | Top nav | User detail | | /ops/settings | AM, SA | Personal settings | Profile | Profile, theme, notifications | Save | Top-nav avatar | β€” |


6. Navigation model per role​

6.1 Client β€” /client/chat (the home shell)​

  • Persistent 256px left sidebar with three stacked groups:
    1. Identity header β€” slug-as-subdomain identity label (e.g. acme.h.work) with the slug initial avatar in claret. This grounds the user in their client portal. (Per CD-12, path-based routing (/client/*) is canonical for navigation; the subdomain host is the public-facing identity per ADR-015.)
    2. Your Specialists β€” list of Specialist personas assigned to the client, each with name + role label ("Primary specialist" / "Specialist"). Click β†’ expert profile preview pane. Active selection: 3px claret left border + accent-tinted bg.
    3. Your Threads β€” chronological thread list. Each row: avatar (Specialist initial), thread label (auto-named from first message or renamed), last-message snippet, relative time, unread count pill. Inline rename (✏️ on hover). Top of group has + New Thread button.
  • Portal footer β€” UserFooter (avatar + name/email, theme toggle, settings links, sign out).
  • Top bar in chat panel β€” Specialist avatar + name + online indicator, channel badge, status pill (read-only for clients), hamburger to toggle sidebar.
  • Composer β€” bottom-fixed input (auto-grow textarea, attach button, send button). Slack-style.

6.2 Client β€” /client/settings/* (separate sidebar)​

  • 220px left sidebar with four sections: Profile Β· Team Β· Channels Β· Billing. No icons β€” text labels only.
  • Top of sidebar has a "← Chat" back-link to return to the conversational shell.
  • Header above the nav shows the client name (e.g. "Acme Financial"), preferring portalContext.orgName and falling back to slug.
  • Active item: 3px claret left border + accent text + accent-subtle bg.
  • Footer: single sign-out (shared with the rest of the app β€” no surface-specific sign-out).
  • Removed in the 2026-05-02 refactor: All Tickets (consolidated into Queue), Analytics, Settings, Integrations (renamed to Channels). Old routes redirect to the closest sensible target (see site map above).

6.3 Expert β€” /workspace/* (top nav)​

  • 48px top header with logo, then horizontal tab bar: Queue Β· Tasks Β· Analytics Β· Settings. Active tab: 2px claret bottom-border + accent text.
  • Right side: user name + avatar (claret) with click-to-sign-out.
  • No left sidebar at the workspace level β€” each route may use its own left rail (e.g. queue list as a 300px split-pane).

6.4 Ops β€” /ops/* (top nav, AM + SA)​

AM and Superadmin share the same /ops/* shell. SA is a strict superset β€” same routes, more nav tabs, broader org visibility (RLS-enforced).

  • 48px top header with H logo + role label ("AM" or "Ops" for SA).
  • AM tab bar: Clients Β· Inbox Β· Settings.
  • SA tab bar: Clients Β· Inbox Β· Queue Β· Analytics Β· Health Β· Billing Β· Team Β· Settings (strict superset).
  • Active tab: 2px claret bottom-border + accent text.
  • Queue tab (SA only) carries a live pending-count pill (danger-colored) sourced from getQueue() polled every 10s.
  • Right side: user name + avatar (claret) β€” same affordance as the Experts workspace.
  • Migrated to the unified /ops/* surface on 2026-05-03 (previously /am/* and /superadmin/*). UserFooter is not used here; the top-nav avatar at top-right exposes the user instead.

7. Global elements (every screen)​

ElementWhere it appearsBehavior
UserFooterInside the chat sidebar for Client. The Experts workspace and the unified Ops surface (AM + SA) use the top-nav avatar at top-right instead (no UserFooter since the 2026-05-03 layout alignment).Displays avatar + name/email, theme toggle (sun/moon, 16px), role-aware footer links (Settings, Billing, etc.), Sign out
Theme toggleInside UserFooterSun icon when dark, moon when light. Click flips [data-theme] and persists hw_theme in localStorage. System preference honored on first visit.
Toast notificationsMounted at root (Toaster in app/layout.tsx)Used for all transient feedback (success, error, info). Fires via toast.success() / toast.error().
TrialBannerTop of all /client/* pages (except /client/chat full-screen and /client/settings/*)Renders only when org is in trial. Shows days remaining, dismissible per-session.
Notification badgeTab title (favicon badge) and on /workspace/queue "Queue" tab pill ("N new")Driven by Socket.io expert_queue_item_added. Cleared on view.
Service workerMounted by ServiceWorkerRegistrarRequired for browser push notifications (when implemented; currently registered but no push handler β€” see USER_STORIES E-P3).
AppNotification feednotifications table + Socket.io notification:new event (#915, shipped 2026-05-25). On client shell: surfaced via the dedicated /client/inbox route (#1866, 2026-06-05; commit b20b36c0). On Expert + AM/SA shells: header bell-icon dropdown.Persistent in-app notification feed (distinct from transient toasts). Read/unread state per user; auto-marked read on click. The client-side NotificationBell drawer was replaced by /client/inbox in #1866.
Error boundaryWraps each top-level route layoutSurfaces a friendly fallback UI on render errors.
BreadcrumbsUsed in /ops/clients/[id]/setup/* (wizard step pills); /client/settings/channels/[id] ("← Back to Channels")NOT used in flat lists β€” drill-down depth is shallow enough that breadcrumbs add noise.

8. Routing & redirect logic​

After login (/)​

getRoleRedirect(platformRole):

platformRoleRedirects to
superadmin/ops/clients
expert/workspace/queue
account_manager / am/ops/clients
owner / admin / member / billing / fallback/client/chat

Cross-prefix guards​

URL prefixLayout guardWrong role gets
/client/*isAuthenticated()Unauthenticated β†’ /login?redirect=... (set by proxy.ts middleware)
/client/settings/* (admin sub-pages: team, channels, billing)requires owner or admin orgRoleRedirected to /client/chat
/workspace/*isAuthenticated() + platformRole ∈ EXPERT_ROLES (= expert, account_manager, superadmin)Unauthenticated β†’ /login; client role β†’ /client/chat. (Closed in 0.3.1 β€” frontend now enforces, API double-enforces.)
/ops/*requires account_manager / am / superadmin; SA-only sub-routes (/ops/queue, /ops/pools, /ops/analytics, /ops/health, /ops/billing, /ops/team) additionally gate on superadminWrong role redirected to /; AM hitting an SA-only sub-route redirected to /ops/clients
/am/*, /superadmin/*legacy β€” redirect to the matching /ops/* route (see Β§4 site map)β€”
/onboarding/*invite-token-gatedNo token β†’ /login?expired=true

Notable redirects​

  • /client β†’ /client/chat (silently)
  • /login while authenticated β†’ role home

9. Information architecture principles​

  1. One conversational home per role. The role's primary verb is reflected at the home route: clients chat (/client/chat), experts triage (/workspace/queue), AMs and superadmins manage clients (/ops/clients). All other routes branch from this home.
  2. Deep work, shallow nav. Every role can reach any of their top tasks in ≀2 clicks. Sidebars surface ~5–7 items max; further depth comes from drill-down, not menu sprawl.
  3. One identity, never expose the team. The client sees only the Specialist persona (e.g. Bob). They never see the underlying AI agent or the human Expert reviewing the draft. All sender attribution in the client UI says "Bob" or the agent's display name; "AI" / "Human review" badges describe the handling, not the identity.
  4. Internal-only details stay internal. Confidence scores, risk levels, internal notes, status dropdowns, and assignment controls are gated on EXPERT_ROLES and never appear in the client UI β€” even though some components (e.g. PortalChat) are shared across roles.
  5. Client identity β‰  platform branding. The client portal sidebar header shows the client identity (acme.h.work), not the h.work platform identity. The platform identity is implicit in everything else. (The acme.h.work host is the public-facing subdomain per ADR-015; the route inside it remains path-based (/client/*) per CD-12.)
  6. Read-only by default for clients. Anything stateful that affects support workflow (status, assignment) is read-only for clients. They observe; experts act.

10. Outstanding IA gaps (tracked in USER_STORIES.md)​

GapAffected route(s)Story ID
Client-facing high-stakes approval banner/client/chat, /client/chat/[id]C-W8 (P2 β€” demoted 2026-05-23; manual approval via chat for MVP)
Business context import wizard/client/onboarding (new step) β€” note: not /client/settings/*, the old /client/admin/settings page was retired in the 2026-05-02 refactorC-O6 (P1)
Agent memory visibility for clientsTBD β€” would need a new admin section if pursuedC-T4 (P2)
SLA performance dashboardTBD β€” would need a new admin section (analytics page was retired)C-V5 (P1)
Invoice download/client/settings/billingC-B3 (P2)
Push notifications (mobile/browser)globalE-P3 (P0)
Native mobile app (expert)globalE-P4 (P1)
Expert profile + expertise tagging/workspace/settingsE-O2/3 (P1)
Inline ticket discussion (expert ↔ expert)/workspace/queue/[id]E-C3 (P2)
Correction categorization UI/workspace/queue/[id] (resolve flow)E-L2 (P1)
Named account lead per client/ops/clients/[id]SA-C9 (P1)
Expert leverage analytics/ops/analyticsSA-E4 (P1) β€” βœ… shipped (#3199)
Per-client channel adapter health/ops/health (per-client breakout)SA-P4 (P1)
Tool permission management UI/ops/clients/[id] β†’ Tools tab (API only; no frontend UI)SA-P5 (P1 gap β€” API exists)
Audit log viewer/ops/clients/[id] β†’ Audit tab (placeholder, no GET /orgs/:id/audit yet)SA-C10 (P1)
Impersonate / reset password/ops/clients/[id] β†’ SA action bar (stubs, no API yet)SA-C9 (P1)

11. Completed migration β€” /portal/* β†’ /client/*​

The client URL prefix migration is complete. All client routes are served from /client/*. Older bookmarks under /portal/* are not preserved at the route layer (no /portal/* directory exists in the app); inbound links from invite emails and old docs were updated as part of the cutover. Sub-route legacy redirects (e.g. /client/admin/team β†’ /client/settings/team) are retained β€” see Β§2 site map.


Thread ↔ Specialist Data Model​

Added 2026-05-02 alongside the introduction of conversations.specialist_id.

Previous model (pre-FK)​

A Conversation carried only a loose agent_name string column β€” assigned at creation by AgentsService.pickAgentName(), which sampled from a per-org agents pool with a hardcoded NAME_POOL fallback. Crucially, the assignment did not consult OrgSpecialistAssignment β€” so a thread for Acme Financial was equally likely to be labeled with the wrong Specialist name as with "Bob" (Acme's actual primary Specialist), and there was no referential link between threads and the specialists table. This was fixed in the 2026-05-02 refactor.

This produced two visible bugs:

  1. Every client thread showed the same arbitrary persona name in the chat header ("Amy Β· Support Specialist") regardless of which Specialist was assigned to the org.
  2. There was no way to filter, sort, or route by Specialist on either the client or expert side.

New model (current)​

Three separate concepts, each with explicit cardinality:

Org ─── M:N ─── OrgSpecialistAssignment ─── 1 ─── Specialist
Β· isPrimary boolean
Β· confidenceThreshold int 0–101

Conversation ─── 0..1 ─── Specialist (new: conversations.specialist_id, nullable FK)
Conversation ─── 1 ─── Org (existing: conversations.org_id)

Expert (User) can map to:
Β· 1..N Specialist personas they staff (via Specialist Team membership)
Β· 1..N Orgs directly (via `expert_access` with `scope='org'`, per ADR-007)
  • Org β†’ Specialist is many-to-many. An org can have 0, 1, or N Specialists assigned. A Specialist may serve multiple orgs concurrently (subject to Specialist.currentOrgId exclusivity if enforced for that persona). Exactly one assignment per org may have isPrimary = true β€” that's the default Specialist new threads are routed to.
  • Conversation β†’ Specialist is optional and at-most-one. New web-channel threads are routed to the org's primary Specialist on creation. Channel-inbound threads (Email / WhatsApp) may stay unassigned until routing decides.
  • Expert β†’ Specialist or Org is the staffing layer. An Expert can be tied to a specific Specialist (responds as that persona, via expert_access with scope='specialist') or attached to an Org (handles all threads for that org under whichever Specialist is assigned to each thread, via expert_access with scope='org'). See ADR-007.

The agent_name string column is retained for backwards compatibility but is no longer the source of truth; when specialist_id is set, agent_name should mirror the Specialist's first name (the service writes both on creation).

What clients see vs. what Experts see​

Clients (the portal at /client/chat):

  • Sidebar header: their workspace name (e.g. "Acme Financial") and a horizontal pill row of all assigned Specialists with the primary marked by a green dot.
  • Thread rows: title from conversation.subject (server-derived from first message) or "New thread", with a small Specialist-initial avatar resolved per-thread from conversation.specialistId. If a thread has no Specialist, the avatar reads "H" (for h.work).
  • No chat-panel header. The client is talking to the h.work service generally; the per-thread Specialist context is conveyed by the avatar in the thread row and (when assigned) the composer placeholder ("Message {Bob}…"). Threads without a Specialist read "Message h.work…".
  • Specialist filter in the sidebar: clicking a Specialist pill scopes the thread list to threads assigned to that Specialist; clicking again (or the "Clear" link) restores the full list.

Experts (the workspace at /workspace/queue):

  • Full visibility into specialist_id, agent_name, the legacy pool, the confidence/risk pipeline, internal notes, status dropdown, and assignment controls.
  • The same PortalChat component renders for an Expert if they navigate to /client/chat β€” but the Expert-only floating overlay (status, assign, channel) appears top-right; clients never see it.

Schema change​

ALTER TABLE conversations ADD COLUMN specialist_id VARCHAR DEFAULT NULL;
CREATE INDEX conversations_specialist_id_idx
ON conversations (specialist_id)
WHERE specialist_id IS NOT NULL;

No FK constraint is enforced at the DB level (cross-module reference; service-layer guard); add one in a follow-up migration if/when the Specialist table layout is locked down for production RLS.

Update (ADR-020, 2026-05-29): Per the isolation classification matrix, customer-PII tables (conversations, messages, expert_queue_items) now carry specialist_id NOT NULL + a CHECK (specialist_id IS NOT NULL) constraint in PR-A..E of #1148–#1153. New rows must always be Specialist-scoped; the NULL-tolerant index above is retained for legacy back-compat rows only.

Limitations / TODOs​

  • Channel-inbound threads still get a Specialist via the legacy agentsService.pickAgentName() path (findOrCreateByChannel). Routing inbound channels to the primary Specialist is the next iteration.
  • Reassigning a thread to a different Specialist is not yet exposed in the UI. Backend update is a one-line column write; UI would live in the Expert workspace ticket detail.
  • Backfill for existing threads. Pre-FK rows have specialist_id = NULL; they continue to render with the "H" fallback initial. A backfill script that maps agent_name to a Specialist by (org_id, first_name) is a P2 cleanup task β€” not blocking.
  • PortalSpecialist (with email) and PortalContextSpecialist (no email) are intentionally separate types in frontend/src/lib/api.ts. The former is returned by GET /client/specialists; the latter is embedded in getPortalContext(). Consider unifying once the surfaces converge.

Owner: Design + Platform team