Skip to main content

INFORMATION_ARCHITECTURE.md — human.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

human.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/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 Plan, usage, 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/billingPlan, usage, paymentManage the subscriptionCurrent plan, usage vs. quota, payment methodUpgrade plan, update cardSettings sidebarStripe 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 human.work.

3. Site map — Expert

/workspace
├── /workspace/queue EXPERT HOME — pending work
│ └── /workspace/queue/[id] Ticket detail (split-pane)
├── /workspace/collab Internal team rooms
├── /workspace/tasks Agentic task plan review (multi-step)
├── /workspace/analytics Personal + pool analytics
├── /workspace/pools Pool membership view
└── /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/collabTeam roomsCoordinate with other expertsRooms list, messages, unread countsSend message, switch roomTop nav
/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 & pool analyticsTrack resolution time, queue depth, AI/Expert mixCharts: volume, resolve time, confidence histogramDate range, pool filterTop nav
/workspace/poolsPool membershipsUnderstand workload distributionPool cards with queue depthOpen poolTop 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/pools [SA only] Expert pool admin
├── /ops/analytics [SA only] Platform metrics
├── /ops/health [SA only] Service health dashboard
├── /ops/billing [SA only] All-org subscriptions
├── /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/pools → /ops/pools
/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
/ops/clients/[id]AM, SAClient detailManage one clientTabs: 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 (live tool-permissions toggles), Audit tab (placeholder).Add member, change specialist, extend trial; SA: change member role, toggle toolsClient listEdit / Wizard / member detail
/ops/clients/[id]/editAM, SAEdit clientCorrect metadataForm: name, contact, country, timezoneSaveClient detailClient detail
/ops/inboxAM, SAPersonal inboxTriage messages addressed to the operatorConversation listOpenTop navConversation detail
/ops/queueSA onlyGlobal queueSpot systemic surgesAll pending tickets across orgs, filtersFilter org / risk / status, drill-inTop navTicket detail (in workspace context)
/ops/poolsSA onlyPool adminMaintain pool compositionPool list, member count, queue depthCreate pool, add/remove memberTop navPool detail
/ops/analyticsSA onlyPlatform analyticsBoard-quality numbersVolume, auto-resolve rate, top request types, active expertsDate rangeTop nav
/ops/healthSA onlyHealth dashboardDetect incidentsService status (API, DB, Redis, channel adapters)Drill into a serviceTop nav
/ops/billingSA onlyAll-org subscriptionsTrack revenue, manage StripeSubscription table, plan tiers, MRRCreate/cancel sub, sync StripeTop navStripe
/ops/teamSA onlyInternal teamManage AMs/expertsUser list with platform roleInvite, change role, deactivateTop navUser detail
/ops/settingsAM, SAPersonal settingsProfileProfile, theme, notificationsSaveTop-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 (e.g. acme.human.work) with the slug initial avatar in claret. This grounds the user in their client portal.
    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 footerUserFooter (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 · Pools · 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).
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 Felix (the AI agent) or David (the human expert). 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.human.work), not the human.work platform identity. The platform identity is implicit in everything else.
  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 (P0)
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)
Per-client channel adapter health/ops/health (per-client breakout)SA-P4 (P1)
Tool permission management UI/ops/clients/[id] → Tools tab (live; backed by /orgs/:orgId/tool-permissions)SA-P5 (P0 — done)
Audit log viewer/ops/clients/[id] → Audit tab (placeholder, no GET /orgs/:id/audit yet)SA-C10 (P1)
Impersonate / reset password / change plan/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 PoolMembership → routing pool → org)
  • 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) or attached to an Org via a routing pool (handles all threads for that org under whichever Specialist is assigned to each thread).

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 human.work).
  • No chat-panel header. The client is talking to the human.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 human.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.

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