Build contract — QI Hub tier (Shape A revised 2026-09-12, issue 2739 / 3050 Track C)
1 · Two features, two migrations, two permission models. Shape A (a multi-QI hub across
firms) and Shape B (offices inside one firm) ship independently and share no code. Anything that would let one
scope helper serve both is a rejection: intra-org grouping must never travel through the helper that crosses the
tenant boundary.
2 · The hub is structurally incapable of being a QI. It is not an
organizations
row, is never a valid
owning_org_id, holds no licence and no bond, and appears on neither side of a
disbursement pair. Enforce it in the schema, not in a guard clause — a guard clause is something a future PR can
delete.
3 · Additive hub tables, a dedicated hub-read boundary, and no widening anywhere else. Three new
sibling tables —
qi_hubs,
qi_hub_links (one active link per QI; unlink is soft, relink
inserts a new row under an active-only unique constraint),
qi_hub_memberships (one read-only
hub_viewer role) — with their own RLS and backstop, read through the
qiHubScope resolver (named apart from the
exchange-wizard
hubScopes helper, which filters inside one tenant); the three tables are API-only
with no direct anon/authenticated
SELECT (our bounded choice, aligned with the Proposed ADR-057
default, not an operating rule). A platform-only read of a firm's standing link record is itself audited before
the response. No
existing table is altered; no existing SELECT policy on any operational table is rewritten or widened for a hub
origin — not
exchanges, not
audit_log, not
org_memberships. The existing
requireTier /
requireOrgAdmin /
requireOrgOwner gates are untouched; the
hub gets its own narrow route boundary whose resolver binds the requested hub, the actor’s active membership and
the selected QI’s active link on every request and fails closed on any miss. Because the API runs on the
service-role key, the route filter is the load-bearing control; RLS on the new tables is defence in depth.
4 · Aggregates cross; records never do. The hub-read response is a strict allow-list: linked-QI
names and count, per-QI open-exchange status counts (sale not yet closed · 45-day date binds next · 180-day date
binds next · no statutory date recorded), statutory-deadline bucket counts on the date that binds each exchange
next, each with its denominator. One shared classifier with tests supplies every bucket including the explicit
no-date category; the hub never derives a bucket by subtraction and never re-implements statutory arithmetic.
Never a party name, exchange reference, address, document, per-person figure, funds amount, signature metric,
audit row or staff roster.
profiles_sensitive keeps its three policies and gains no fourth. The
aggregate is one statement that starts from the hub’s authorized links and left-joins the counts, so a linked QI
with nothing open is an explicit zero group and a missing group cannot be mistaken for a retrieval failure; the
only failure is the statement itself, and it fails the whole request rather than returning a partial sum. There is no drill-down: a QI workspace is entered only through a role the
person separately holds in that firm, under that role’s own permissions — the link is rendered from the actor’s
own org memberships as already resolved for their session, intersected client-side with the linked-QI list; the
hub route never queries
org_memberships and the rollup carries no per-actor field.
5 · Money is untouchable from here. No path on this tier proposes, approves, releases or
redirects a disbursement; two authorized officers of the owning firm, proposer distinct from approver, authorize
it, and an exchanger has no fund-movement path anywhere in the product.
6 · Statutory clocks are not administrable. The 45- and 180-day dates move only inside the owning
firm with a recorded basis — never from this tier. An overdue bucket renders in the alarm state while it is
non-zero and must never resolve to a neutral or satisfied appearance because it was acknowledged or scrolled
past. Bucket arithmetic reuses the QI Ops deadline classification (Pacific dates, effective exchange-period
bounds) extended into one shared classifier that returns every bucket
and the no-date category; the hub
never re-implements it and never labels a column as a concurrently running clock — each exchange is counted once
on the date that binds it next.
7 · Identity is the first gate and it belongs to the person. No role at any tier can mark KYC
verified for an exchanger, and no “sign on behalf” affordance may exist anywhere — eSign Act and UETA require the
signer’s own intent, so the control is absent rather than permission-gated.
8 · Shared configuration is deferred, not designed. Nothing on this surface publishes, inherits
or resolves a setting. Any hub default is a later, reviewed authority and provenance decision against the shipped
tiered workflow configuration (ADR-053) — no second resolver, no write-through, no fee schedule, credential or
tenancy election ever inherited from a hub role.
9 · People administration is deferred, and its gate is legal, not technical. This slice has no
write path from the hub into any firm. Platform administrators provision hubs, link and unlink QIs, and grant and
revoke hub memberships; a hub cannot widen its own scope. Cross-QI staff administration waits on a STATEQI-003
determination with counsel; when designed it uses the firm’s real ladder (
org_staff →
org_admin →
org_owner), refuses self-targeting and owner or platform grants, and never
modifies an existing owner row. Hub membership and firm membership are separate grants: revoking one never
deletes the other, and neither confers the other.
10 · One scope selector, its own control. All linked QIs or one selected QI, as a labelled select
with a live announcement and a persistent breadcrumb segment. The QI Ops queue’s
My files / My team
segment is not reused for its semantics — those resolve a person’s book and office inside one firm — and a
firm’s office filter never widens across firms. Unknown or unauthorized scope in the URL resolves to a
non-enumerating unavailable state, never silently to all QIs. Loading and error states show no figure from a
previous scope. A revoked link or membership removes the QI and its figures
from the payload,
server-side — never by concealing them in the view — and the M4 gate asserts on the response body and the
emptied client cache, not on rendered text; membership revocation also removes the hub name and the membership
role from the chrome and collapses the breadcrumb to the tier name. The built live region and breadcrumb set the
firm name with
textContent (React text children), never markup:
scopeName is
tenant-supplied; state colour is set by class, never by an inline
style; the table scroll cue is
measured by a
ResizeObserver that is disconnected on unmount, not by a bare resize listener; a
successful read reflects the current authorized set and never infers an unlink event from an older cache — there
is no hub-side "link ended" notice. All of this holds on the next request, with no token refresh or re-login required.
11 · Provenance is stated in these notes, not on the surface. The multi-QI hub is designed, not
shipped; no screenshot of it may be narrated as a shipped capability. Product copy carries no build-status label
(AGENTS.md § Product UI copy); the tier’s product name is QI Hub (issue 3026) and every occurrence renders
through
.tier-name. A review of this file reads the states through
?hubState=, the
Shape B posture states through
?state=.
12 · Firm compliance posture is Shape B, read-only, and the S5 canon. Section 10 (issue 3248,
founder ruling D-6 on issue 2739) is what the QI Hub compliance page is built from: four read-only cards that
mirror the QI Ops
ComplianceSection — State QI Licenses, Bond / E&O Coverage, Compliance Program
Cadence, Control Register — with every label reused verbatim from
packages/web-app/src/pages/admin/qi/ComplianceSection.tsx, the cadence urgency pill from
lib/cadenceUrgency.ts, and every card linking to the QI Ops page that owns the record. Absent by rule:
last-verified stamps, continuous-check claims, history or trend figures (issue 2678), fund or signature figures,
and any cross-org language. The register card never loads, empties or errors because it ships with the app.
No cross-QI posture is drawn on this surface.
Fully tokenized against
../tokens.css — no raw hex, no inline color outside token references. Light
and dark are both designed. The tier accent
--tier is
[PROPOSED]: add it to
packages/web-app/src/index.css in
:root and
.dark before writing a call
site. All firms, people and figures in the Shape A sections are synthetic; the unlinked third QI in the fixture
never appears here. Sections 9–10 (Shape B) are preserved legacy content outside this revision's scope.