Proposed (STA-916, batch stackra-21). The batch reserved 0444, which ADR 0444 (one self-hosted
tools stack per region) took first; this is the next number after 0456, the last one taken.
Context
The Commerce wave A build listed two gaps (Commerceapps/docs/src/docs/build/module-catalogue.mdx, “Stackra gaps”). The
container has no extension point or multi-binding: a class token resolves to one instance, so a module that builds on
another (personalisation or b2b on carts) cannot contribute a guard to its checkout gate without the gate’s owner
listing every contributor in code. And no package adds members to @stackra/navigation’s open RouteServices, so a
route context carries no session and a console, till or mobile layout cannot gate on one. The signed edge-header check
(stackra-65, ADR 0437) needs the same kind of extension point on the server: a check every inbound request passes.
The alternatives weighed were a multi token (multi(CheckoutGuard), or an array type gen reads from a constructor
parameter), rejected because classes are the only tokens (ADR 0294 decision 17) and it needs a new wiring section gen
must write; a contribution decorator (@Contributes(CheckoutGuard)), rejected because a decorator only records
(ADR 0305) and subclassing already says the same thing; and identity owning the RouteServices augmentation, deferred
because the route needs the session per request on a Worker, which only the app runtime’s request loop can load.
Decision
- A contribution is a subclass, and
getAll(Base)lists them. The module that owns an extension point declares an abstract base (abstract class CheckoutGuard extends Service); a module that contributes binds a subclass, as gen binds any class.Container.getAll(Base)returns every class the booted modules bind or export that extendsBase, resolved through the normal recipe (overrides,@Replaces, lifetimes), in wiring order: dependencies first, the host last.Baseitself is never listed; a contribution from a module that did not boot is not listed; aLifetime.Requestcontribution needs aforRequest()container.DiscoveryService.getAll(Base)gives a service the same list through its constructor. No new token type, no wiring section, no gen change. - The server’s request guards are one such extension point.
RequestGuard(@stackra/app/server) is an abstractServicewithcheck(request)and anorder(default 0; the place a guard holds in a fixed check sequence such as ADR 0437’s server-check order, never a business value).ServerApp.handlers.guard(request, container)runs every contributed guard on the call’s request container, lowest order first and wiring order within an order, and rejects with the first refusal; later guards do not run. The generated adapter calls it insidehandlers.run. @stackra/appaddssessionandidentitytoRouteServices.RouteSessionaugments navigation’sRouteServiceswithsession: SessionUser | nullandidentity: SessionStore | undefined, both@stackra/identity/contractsshapes. The Worker target loads the session from the boundSessionStoreinto each request container before the member’s context factory runs; the browser and native targets load it once at boot. Each target contributes one route context part that reads it. No bound store, or a refusal from the store (a revoked session), is signed out; any other failure rejects, and the target reports it.
Consequences
- Commerce’s carts declares its checkout guard base and reads
getAll; personalisation, b2b and the till contribute without carts knowing them. stackra-65 contributes its edge-header guard as aRequestGuardwith a negative order. @stackra/appdepends on@stackra/navigationand@stackra/identity(no cycle: no package depends on@stackra/app); the lockfile row is the integration batch’s (stackra-90).- The Worker’s member context factory must hand the routes the request container it was given, or
ctx.sessionis null. The browser and native session is loaded once per page load or launch: the web’s sign-in and sign-out leave the page, and a device flow that signs in or out without a reload must re-boot or call for a refresh (a later change). - A Worker that binds a
SessionStoreloads the session on every request, including public pages; a storefront that needs no session binds no store. - Navigation’s README and
RouteServicesdocblock still say identity adds the members; stackra-90 updates them.
Enforcement
packages/foundation/container/src/core/services/__tests__/container.service.test.ts (“getAll (multi-binding)”:
wiring order, unbooted modules left out, overrides honoured, the base never listed, request lifetime) and
discovery.service.test.ts; packages/runtime/app/src/server/functions/__tests__/create-app.function.test.ts
(“guard”: order, first refusal stops the rest, no guard lets through);
packages/runtime/app/src/core/functions/__tests__/route-session.function.test.ts (per-container session, refusal is
signed out, other failures reject, no store) and the Worker create-app.function.test.ts (the request container
carries the session); packages/runtime/app/src/core/__tests__/index.test.ts (the augmentation’s types).
Supersedes and sources
Supersedes: none. Amends ADR 0294 decision 17 (classes are the only tokens: a base class now also names the set of
its booted subclasses, read with getAll). Sources:
Linear STA-916; Commerce apps/docs/src/docs/build/module-catalogue.mdx (container and app rows); ADR 0004, 0206, 0263,
0305, 0437.