Action Items
Open items the team needs to act on — a decision, a design to review, or a document to produce — surfaced by reviews of this policy but not yet resolved. This is the single place they're tracked; the policy and Annex A link here rather than repeating the full explanation in multiple places. Once an item is resolved, move its outcome into the policy/Annex A directly and mark it Closed here rather than deleting the row — the history of why a decision was made is worth keeping.
Types: Decision (needs an Architecture Review Board or governance call) · Design (needs a technical design reviewed/accepted) · Documentation (needs an artifact produced) · Implementation (needs code written, pattern already decided).
Status: Open · Proposed (a concrete answer has been drafted, pending sign-off) · Closed.
Tracking table
Quick-scan index of every item below. Dates are only filled in where the source text states them explicitly — — means genuinely unrecorded, not zero; backfill as the real dates become known rather than leaving a guess in place.
| ID | Item | Type | Status | Owner | Added | Due | Closed |
|---|---|---|---|---|---|---|---|
| ACT-001 | Cross-tenant admin-read access mechanism | Design + Decision | Proposed | — | — | — | — |
| ACT-002 | Data Ownership Matrix | Documentation | Open | — | — | — | — |
| ACT-003 | Event bus durability guarantee | Decision | Open | — | — | — | — |
| ACT-004 | Mirror rarit-saas-kernel ADRs into this site? | Decision | Closed | — | — | — | 2026-08-26 |
| ACT-005 | Configure Claude Code Review on kernel/vertical repos | Implementation | Closed | — | — | — | 2026-08-28 |
| ACT-006 | REVIEW.md drift-prevention across repos | Implementation | Open | — | — | — | — |
| ACT-007 | Roll out PR template & branch-naming convention platform-wide | Implementation | Open | — | — | — | — |
| ACT-008 | Queue status board form factor — phone screen or waiting-room kiosk/TV | Design | Open | — | 2026-09-11 | — | — |
| ACT-009 | Consumer Identity extension for patient/parent cross-tenant access | Design + Decision | Open | — | 2026-09-11 | — | — |
| ACT-010 | Stale withTenantScope() anchor text in 4 links to Annex A.10 | Documentation | Open | — | 2026-09-11 | — | — |
| ACT-011 | Branch/campus modeling (multi-location tenants) | Design + Decision | Proposed | — | 2026-09-15 | — | — |
| ACT-012 | IAM: a role change is not effective until the user's token refreshes | Implementation | Open | — | 2026-10-17 | — | — |
| ACT-013 | IAM: a database outage is answered 401 and leaks the raw database error | Implementation | Open | — | 2026-10-17 | — | — |
| ACT-014 | IAM: POST /invitations/accept answers 500 | Implementation | Open | — | 2026-10-17 | — | — |
| ACT-015 | platform-core: the connection pool has no error listener | Implementation | Open | — | 2026-10-17 | — | — |
| ACT-016 | platform-core: an exception from hasPermission escapes route access as a bare 500 | Implementation | Open | — | 2026-10-17 | — | — |
| ACT-017 | Search Engine integration decisions (record for ratification) | Decision | Proposed | — | 2026-10-17 | — | — |
ACT-001 — Cross-tenant admin-read access mechanism
Type: Design + Decision · Status: Proposed · Owner: _____________
Raised by: Annex A.11.2 (prior-art evaluation), confirmed as a real blocker via Tourism backlog story TEN-DASH-01
The problem: withTenant(tenantId, work) opens exactly one RLS-scoped transaction for one tenant, by design. There is no way to ask it for "every tenant," so any admin/ops tool needing a cross-tenant view — starting with Tourism's own committed Super Admin dashboard story — cannot be built under the pattern as currently specified.
Proposal: a separate withPlatformAdminScope(actor, reason, work) function (never a parameter on withTenant(), to avoid a magic-null footgun), with four properties: (1) authorization checked against the actor's IAM role before any query runs; (2) read-only by default — a cross-tenant write would need its own, more tightly gated function; (3) the RLS bypass added only to the specific tables an admin view needs (tenant metadata, plan/status), never blanket-applied to business data; (4) reason required and logged via logAction() before work runs, so every use has an audit trail. Full code and the concrete TEN-DASH-01 example: Annex A.11.2.
Open sub-questions this proposal does not resolve on its own:
- Who is granted the
platform_adminIAM role, and is granting it itself a logged, approval-gated action? - Standing role, reviewed periodically, or time-boxed/re-auth per session (just-in-time elevation)?
ACT-002 — Data Ownership Matrix
Type: Documentation · Status: Open · Owner: _____________ Raised by: Formal architecture review (changelog v1.4)
The problem: the policy states the principle (each engine owns its own tables; no cross-engine or cross-vertical direct table access) but no document enumerates it table by table. "Which engine owns this data" is currently answered by convention and code review, not by something anyone can check — the same class of risk Section 4's acyclic-dependency rule and Section 5's getDb()/withTenantScope() mandate already guard against by other means.
Proposal: produce a matrix (engine/vertical × table × owner × read/write access) as an approved artifact. Assigned to the Architecture Review Board per Policy §9; no draft exists yet to link to.
ACT-003 — Event bus durability guarantee
Type: Decision · Status: Open · Owner: _____________ Raised by: Formal architecture review (changelog v1.4)
The problem: @rarit-kernel/event-bus's emitEvent()/onEvent() (used to break the IAM↔Notifications and Entitlements↔Notifications cycles in Annex A.7) solve the import-direction problem but say nothing about delivery guarantees. If it's an in-process emitter, a crash between an event committing and its subscriber running silently drops the event — for OTP that's a UX problem, for anything touching Payment or Audit it's a correctness problem.
Proposal: confirm with whoever owns @rarit-kernel/event-bus whether it's in-process or durable. If in-process, adopt a transactional outbox (event payload written to an outbox table in the same transaction as the triggering change; a separate worker dispatches from there) for OTP/payment/audit-sensitive events specifically — lower-stakes events may not need the same guarantee. Full reasoning: Annex A.7.
ACT-004 — Should the rarit-saas-kernel ADRs be mirrored into this site?
Type: Decision · Status: Closed · Owner: _____________ Raised by: Direct question during Annex A review
The problem: Policy §5 and §6 both cited db-access-pattern-decision.md via a main-branch GitHub URL into the rarit-saas-kernel repository. Two real risks: the link wasn't validated by this site's own build the way every internal link is, and a main-branch link silently points at whatever the file becomes later. The link also turned out to be a single point of failure in practice — the source file became unavailable.
Decision: mirrored into this collection as ADR: How a Repository Reaches the Database, Version 1.0, dated to the original's 2026-08-26 acceptance. This supersedes the original "don't copy, just pin the link" recommendation below, kept for the record of what was considered. The mirror is an explicit snapshot, not a live sync — if rarit-saas-kernel's copy changes, this page needs a manual update and a version bump, the same discipline signed-records/ already applies to every other accepted document. Policy §5, §6, and Annex A.10 now link to the local mirror instead of the external URL.
Original proposal (superseded, kept for context): recommend against copying the ADR files into this repo, and instead pin the existing GitHub links to a commit SHA or tag, relying on Annex A.10's self-contained restatement to carry most of the value. Superseded because a broken/lost external source is a worse outcome than a manually-synced mirror — the two-copies-drift risk is real but judged less costly than link rot for a document this load-bearing.
ACT-005 — Configure Claude Code Review on kernel and vertical repos
Type: Implementation · Status: Closed · Owner: _____________
Outcome: Resolved by the new
Code Review & Pull Request Policy
(v1.0, 2026-08-28), which formalizes trigger behavior ("Once after PR
creation"), the CLAUDE.md/REVIEW.md split, severity vocabulary, and
advisory-only enforcement. See that policy for the current, authoritative
version of this configuration rather than this entry's original proposal
text.
ACT-006 — REVIEW.md drift-prevention across repos
Type: Implementation · Status: Open · Owner: _____________ Raised by: Code Review & Pull Request Policy §4
The problem: each kernel/vertical repo's REVIEW.md carries a shared
base of architecture rules, currently propagated by copy-paste. A change
to the shared rules (e.g. a new Annex A escalation) requires manually
re-copying into every repo's REVIEW.md; nothing detects or flags drift
if a repo is missed.
Proposal (not yet designed): candidates include a small generation
script that assembles each repo's REVIEW.md from a shared base file
plus a repo-specific fragment (checked into the kernel repo, published
as part of CI or a pre-commit step), or a periodic manual audit as an
interim measure. No decision made yet.
ACT-007 — Roll out PR template and branch-naming convention platform-wide
Type: Implementation · Status: Open · Owner: _____________ Raised by: Code Review & Pull Request Policy §2, §5
The problem: .github/PULL_REQUEST_TEMPLATE.md and the
<PROJECT>-<number> branch-naming convention are specified but not yet
confirmed present across all repos in scope — including the independent
mobile apps (Dw2ni, Adl, Hajez, Lasscar, Rizq, Bindoo, CashBlash, Axil),
which have no existing rollout plan at all since they sit outside the
Architecture Policy's own scope.
Proposal: confirm/add the template and branch-naming convention to
each repo; for mobile apps, this also depends on finishing their
REVIEW.md files (see prior conversation — state management, networking
layer, and persistence choice still need confirming per app before their
REVIEW.md can move past draft status).
ACT-008 — Queue status board form factor: phone screen or waiting-room kiosk/TV
Type: Design · Status: Open · Owner: _____________
Raised by: Clinivo Sprint 1–2 mobile team planning; confirmed via Jira that CLIN-305 ("Update patient queue status (mobile)") is a staff action screen, not a public display, leaving APQ-05/CLIN-185 ("Queue status board") unrepresented by any mobile- or kiosk-specific ticket.
The problem: "Queue status board (waiting/in-progress/done)" reads like a waiting-room TV/kiosk display, but its only implementation (CLIN-185, Done) and the newer mobile queue stories (CLIN-303, CLIN-305) are both staff-facing phone/web views. Nothing currently addresses a genuinely public-facing waiting-room display, and building one as "just another mobile screen" risks the wrong form factor — a phone a staff member carries isn't a waiting-room TV.
Proposal (not yet designed): a short spike to confirm with clinic operations stakeholders whether a physical waiting-room display is actually wanted for MVP, and if so, whether it's (a) a dedicated public, unauthenticated "display mode" build target within rarit-clinivo-mobile, (b) a separate lightweight web page meant to be opened on a TV browser, or (c) out of scope for MVP, with the two current staff-facing queue views (CLIN-303, CLIN-305) considered sufficient on their own.
ACT-009 — Consumer Identity extension for patient/parent cross-tenant access
Type: Design + Decision · Status: Open · Owner: _____________
Raised by: Clinivo patient-EMR-access discussion; grounded in two existing precedents — PHY-INT-01 (Clinivo, physician cross-tenant identity via the Auth Engine's IAM-16–IAM-19) and PORTAL-04 (EduSuite, parent multi-tenant workspace switcher, Phase 2).
The problem: the Auth Engine's cross-tenant identity mechanism (IAM-16–IAM-19) is proven for staff identities (a physician working at multiple tenants) and already assumed, independently, for a consumer identity (a parent linked to multiple EduSuite tenants in PORTAL-04) — but the IAM Engine SRS's own Target Users section never names a consumer/non-staff persona category. Clinivo's own patient-facing features (a patient viewing their own EMR, or granting/revoking cross-tenant sharing consent per CON-01–04) have no identity model to build against as a result.
Proposal: register a generic "Consumer Identity" persona category in the Auth Engine, extending IAM-16–IAM-19 rather than building a vertical-specific mechanism; each vertical declares its own consumer persona (Patient for Clinivo, Parent for EduSuite) in its role_manifest. Full reasoning and both precedents: see Clinivo_Cross_Tenant_Consumer_Identity_Note.md.
Open sub-questions this proposal does not resolve on its own:
- Should a patient's consumer identity be readable by default at a new tenant relationship, or opt-in/consent-gated from the start — ties directly to
CON-01–04? - Does SAFAR have the same latent need (a traveler with bookings across multiple agencies)? Not confirmed, flagged only.
- Is
CON-01's consent capture already assumed to be self-service (patient logged in) or staff-recorded on the patient's behalf? Affects whether this blocksCON-01–04's backend contract work too, or only the new EMR-viewing work.
ACT-010 — Stale withTenantScope() anchor text in 4 links to Annex A.10
Type: Documentation · Status: Open · Owner: _____________
Raised by: npx docusaurus build warning (onBrokenAnchors) surfaced while fixing the ACT-008/009 MDX build break — unrelated to that bug, left unfixed there deliberately.
The problem: 4 links across the site point at annex-a#a10-tenant-scoped-database-access--the-getdb--withtenantscope-pattern. That section was renamed at some point to reflect the real API (withTenant() / withPlatformScope(), not getDb()/withTenantScope()), but the anchor text embedded in the linking pages' visible link labels was never updated to match — so the pages currently say one thing and point at a heading now describing another. Not a broken build (Docusaurus's onBrokenAnchors only warns by default), but a real content inconsistency for anyone reading those pages.
Proposal (not yet decided): this needs a content call, not a guess — specifically, whether the surviving link text should say withTenant()/withPlatformScope() (matching the renamed target) or something else, and whether it's worth doing alongside or independently of the broader getDb()/withTenantScope() naming cleanup already tracked via the db-access-pattern-decision.md ADR fix.
ACT-011 — Branch/campus modeling (multi-location tenants)
Type: Design + Decision · Status: Proposed · Owner: _____________
Raised by: Kernel backlog audit; EduSuite Super Admin Console story ESA-03 (Branch Management screen — UI shell, sample data only)
The problem: ESA-03 ships a Branch Management screen with no backing data model — the story text states outright that no engine currently represents a "branch" as an entity distinct from a tenant. This isn't EduSuite-specific: the same shape recurs wherever an organization operates multiple physical locations under one subscription — a school with several campuses (EduSuite), a hospital group with several facilities (Clinivo), a travel agency with several branch offices (SAFAR). Left unresolved, each vertical is likely to invent its own ad-hoc "branch" representation, which is exactly the kind of silent per-vertical divergence Annex A's ownership rules exist to prevent — and it parallels the cross-vertical identity gap already tracked in ACT-009 for Consumer Identity.
Proposal — four candidate implementations, none yet selected:
-
Branch as a child tenant (parent-tenant hierarchy). Add an optional
parent_tenant_idto the existingtenantstable. Each branch is a full tenant, sowithTenant(tenantId, fn)and RLS need zero new mechanism — a branch is scoped exactly like any tenant today. Entitlements/billing would need a rule for whether a branch inherits its parent's plan or needs its own subscription (open sub-question). Risk: "tenant" stops meaning "one paying customer," which several existing assumptions (EntitlementsENT-08locked-price-per-tenant, Payment dunning) may implicitly rely on. -
Branch as a new shared kernel entity in
platform-core(e.g. abranchestable withtenant_idFK, owned by platform-core, exposed via a small CRUD API). Keeps "tenant" meaning intact; a branch is a sub-resource of a tenant, not a peer of one. Every vertical gets branches for free without re-inventing it. RLS would scope bytenant_idas today, with an additionalbranch_idfilter applied at the application layer where relevant. This is the most reusable option and the most consistent with how Consumer Identity (ACT-009) is heading — a shared, cross-vertical primitive rather than a per-vertical one-off. -
Branch as an EduSuite-specific module. Fastest path to unblock
ESA-03alone, but reproduces the branch concept independently in Clinivo and SAFAR later, with no guarantee the three implementations agree on shape — the outcome this proposal is trying to avoid. -
Branch as an IAM sub-resource (child of a tenant inside the IAM engine, alongside users/roles). Natural fit if branches mainly need their own admin/staff scoping (e.g. branch managers who shouldn't see other branches), less natural if branches are primarily a reporting/entitlements grouping rather than an access-control boundary.
Recommendation (non-binding): option 2 (shared platform-core entity) best matches the "reusable, cross-vertical primitive" pattern already adopted for Consumer Identity, and avoids overloading tenant semantics the way option 1 would. Final call is an ARB decision.
Open sub-questions this proposal does not resolve on its own:
- Does a branch get its own entitlements/subscription, or does it always inherit its parent tenant's plan?
- Is branch-level access control (a user restricted to one branch) in scope for the same decision, or a separate follow-on?
- Does this block
ESA-03moving out of UI-shell status, or can a minimal version ship first with the full model decided later?
ACT-012 — IAM: a role change is not effective until the user's token refreshes
Type: Implementation · Status: Open · Owner: _____________
Raised by: the Search Engine end-to-end proof (packages/search/tests/e2e/e2e.test.ts), Search story SRC-22 (SASK-437)
The problem: PATCH /api/v1/iam/users/:userId/role answers 200, yet the same user's access token keeps the old role: the session reads the role from the token, so a revoked permission stays usable until the token is refreshed. The Search proof reassigns a user to a role without patients.read and the same token still searches. A permission removed from the role manifest, by contrast, takes effect on the very next request.
Proposal (not yet decided): read the membership's role from the database on every request (or on a short cache with invalidation on role change), as hasPermission already does for the permission rows. Interim: document the token lifetime as the revocation delay.
Note: the test is pinned with it.fails ("reassigning a user's role takes effect on the same token's very next call"). When this is fixed it starts to fail, and the .fails must be flipped. Owner of the pin: Search.
ACT-013 — IAM: a database outage is answered 401 and leaks the raw database error
Type: Implementation · Status: Open · Owner: _____________
Raised by: the Search end-to-end proof, section g (docs/E2E_TRANSCRIPT.md)
The problem: with the database down, IAM's session lookup fails first and answers 401 unauthorized (iam.unauthorized), so a caller is told to log in again instead of that the service is unavailable, and the body carries the raw error text (database "…" is not currently accepting connections, Connection terminated unexpectedly). The result is closed (no data) but the status is wrong and the message is an information leak.
Proposal (not yet decided): map a non-AppError failure of the session lookup to 503 access_control_unavailable and never copy the underlying message into the body; log name and code only.
Note: pinned with it.fails ("a stopped database answers 503, not 401"); flip when fixed.
ACT-014 — IAM: POST /invitations/accept answers 500
Type: Implementation · Status: Open · Owner: _____________
Raised by: the Search end-to-end proof (probe recorded in docs/E2E_TRANSCRIPT.md: invite 201, accept 500)
The problem: the route is public, so it runs in a platform scope, and acceptInvitation then writes an audit row for the invited tenant. audit_log's row level security refuses a tenant row from a platform scope (new row violates row-level security policy), so accepting any real invitation fails with 500. Existing tests only post a bogus token, which fails earlier.
Proposal (not yet decided): open the tenant scope of the invitation's tenant for the user and audit writes, after the token is resolved, as runPerTenant does for other platform enumerations.
ACT-015 — platform-core: the connection pool has no error listener
Type: Implementation · Status: Open · Owner: _____________ Raised by: the Search end-to-end proof, section g
The problem: pg.Pool emits error when it drops an idle client (a database restart or failover, 57P01). initDatabase attaches no listener, so Node treats it as an uncaught exception and ends any process without its own handler. The proof installs a handler for the duration and records the exceptions.
Proposal (not yet decided): attach pool.on("error", …) in initDatabase that logs name and code and lets the pool replace the client.
ACT-016 — platform-core: an exception from hasPermission escapes route access as a bare 500
Type: Implementation · Status: Open · Owner: _____________
Raised by: Search (packages/search/docs/PROPOSALS.md, section 2, has the full issue text)
The problem: route-access.enforce.ts awaits AccessControl.hasPermission, authorizeSupportAccess and resolveSession with no handling. A non-AppError thrown by IAM becomes the generic 500, not access_control_unavailable (503). Search converts it in its own route scope; every other engine would need the same.
Proposal (not yet decided): wrap each port call: pass an AppError through, turn anything else into the 503 refusal, log name, code, route and request id (never the message).
ACT-017 — Search Engine integration decisions (record for ratification)
Type: Decision · Status: Proposed · Owner: _____________
Raised by: merged PRs #375 and #376 of rarit-saas-kernel
Decisions taken (each is in the code and its tests):
- A. Platform tenant discovery uses six SECURITY DEFINER reader functions, one NOLOGIN reader role per table (ADR
platform-enumeration-under-rls); a policy keyed onapp.scopewas rejected. - B.
registerSearchContextResolversupplies row-scope values only; permissions come from IAMhasPermission, once per key per request, never cached; tenant and actor come from the session. - C. Manifest
rowScopeaccepts the SRS string form and{ name, field }; the string is normalised to{ name: s, field: s }. - D.
SearchErrorextendsAppError;messageKeyandparamskept. - E. Catalogs live in
locales/(namespacesearch);searchCatalogDiris exported. - F.
searchis inscripts/npm-packages.mjsand the root build chain. - G. No
reasonor sensitive actions forsearch.*audit entries. search.enabledis a boolean entitlement, never metered, checked withcheckEntitlement(a baresearchkey resolves to nothing).- Query auditing records entity, query length, term count, filter field names and total, never the query text.
Pending Ossama's confirmation (not decided): TSS-REG-019 wording (a different owner is rejected; the same owner with a changed schema is accepted and triggers a rebuild); SRC-28 lag per entity (keep health engine-wide, add per-entity lag as a metric label when metrics are wired).