Skip to main content

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.

IDItemTypeStatusOwnerAddedDueClosed
ACT-001Cross-tenant admin-read access mechanismDesign + DecisionProposed————
ACT-002Data Ownership MatrixDocumentationOpen————
ACT-003Event bus durability guaranteeDecisionOpen————
ACT-004Mirror rarit-saas-kernel ADRs into this site?DecisionClosed———2026-08-26
ACT-005Configure Claude Code Review on kernel/vertical reposImplementationClosed———2026-08-28
ACT-006REVIEW.md drift-prevention across reposImplementationOpen————
ACT-007Roll out PR template & branch-naming convention platform-wideImplementationOpen————
ACT-008Queue status board form factor — phone screen or waiting-room kiosk/TVDesignOpen—2026-09-11——
ACT-009Consumer Identity extension for patient/parent cross-tenant accessDesign + DecisionOpen—2026-09-11——
ACT-010Stale withTenantScope() anchor text in 4 links to Annex A.10DocumentationOpen—2026-09-11——
ACT-011Branch/campus modeling (multi-location tenants)Design + DecisionProposed—2026-09-15——
ACT-012IAM: a role change is not effective until the user's token refreshesImplementationOpen—2026-10-17——
ACT-013IAM: a database outage is answered 401 and leaks the raw database errorImplementationOpen—2026-10-17——
ACT-014IAM: POST /invitations/accept answers 500ImplementationOpen—2026-10-17——
ACT-015platform-core: the connection pool has no error listenerImplementationOpen—2026-10-17——
ACT-016platform-core: an exception from hasPermission escapes route access as a bare 500ImplementationOpen—2026-10-17——
ACT-017Search Engine integration decisions (record for ratification)DecisionProposed—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_admin IAM 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 blocks CON-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:

  1. Branch as a child tenant (parent-tenant hierarchy). Add an optional parent_tenant_id to the existing tenants table. Each branch is a full tenant, so withTenant(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 (Entitlements ENT-08 locked-price-per-tenant, Payment dunning) may implicitly rely on.

  2. Branch as a new shared kernel entity in platform-core (e.g. a branches table with tenant_id FK, 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 by tenant_id as today, with an additional branch_id filter 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.

  3. Branch as an EduSuite-specific module. Fastest path to unblock ESA-03 alone, 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.

  4. 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-03 moving 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 on app.scope was rejected.
  • B. registerSearchContextResolver supplies row-scope values only; permissions come from IAM hasPermission, once per key per request, never cached; tenant and actor come from the session.
  • C. Manifest rowScope accepts the SRS string form and { name, field }; the string is normalised to { name: s, field: s }.
  • D. SearchError extends AppError; messageKey and params kept.
  • E. Catalogs live in locales/ (namespace search); searchCatalogDir is exported.
  • F. search is in scripts/npm-packages.mjs and the root build chain.
  • G. No reason or sensitive actions for search.* audit entries.
  • search.enabled is a boolean entitlement, never metered, checked with checkEntitlement (a bare search key 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).