BlaC’s registry is a module-level singleton. In the browser that is exactly what you want: every useBloc call across your app resolves the same (class, key) to the same instance, so state is shared with no providers. On a server, that same singleton is a problem — one process handles many requests, and a shared registry means one user’s bloc state can bleed into another user’s request.
This page names that hazard plainly and shows the fix that already ships: swapping the active registry per request via setRegistry / getRegistry, so each request gets its own isolated set of bloc instances.
On the client there is one user and one registry, for the life of the tab. On the server there is one process and one module-level registry (globalRegistry), but many concurrent users. Every registry function — acquire, ensure, borrow, release, and watch (which resolves via ensure) — routes through a single module-level variable inside @blac/core:
If nothing swaps _registry, every request shares globalRegistry. Concretely:
Request A renders for user A, acquire(SessionCubit) creates an instance keyed 'default' and fills it with user A’s data.
Request B renders for user B. acquire(SessionCubit) finds that same'default' instance already on the shelf and reuses it — now serving user A’s session data to user B.
This is not a re-render bug or a hydration mismatch. It is a correctness and security hole: server-rendered HTML for one user can contain another user’s state. The same goes for any keyed instance whose key is not perfectly request-unique — and for keepAlive blocs, which by design survive ref count zero and therefore persist across requests in a long-lived process.
The registry class. new StateContainerRegistry() is an empty registry with its own instance map.
Construct one per request.
setRegistry(registry)
Sets the module-level active registry.
Point the active slot at the request’s registry.
getRegistry()
Returns the current active registry (initially globalRegistry).
Read it back if you need to operate on the active one directly.
globalRegistry
The default singleton, used until setRegistry is called.
The value you restore the slot to after a request.
A fresh StateContainerRegistry starts empty: no registered types, no instances, no refs. Bloc classes are not pre-registered against a registry — the first acquire/ensure of a class against whichever registry is active registers and creates the instance there. So once setRegistry(requestRegistry) runs, the very first acquire(SessionCubit) in that request creates a brand-new SessionCubitinside requestRegistry, isolated from every other request.
setRegistry is a single global slot, so the naive “set at the start of the handler, restore at the end” only works if nothing async interleaves between two requests. Under real concurrency — awaiting a data fetch mid-render while another request runs — a bare set/restore races: request B’s setRegistry clobbers the slot while request A is suspended.
Node’s AsyncLocalStorage is the standard tool for request-scoped values that survive await. BlaC does not ship an AsyncLocalStorage integration — the exports above are the primitives. The wiring below is an application-level pattern you build on top of setRegistry / getRegistry: keep the per-request registry in AsyncLocalStorage, and make a tiny shim so getRegistry resolves the registry for the current async context rather than a single shared variable.
On the client there is one registry for the whole tab, so isolation is a non-issue — do not call setRegistry in browser code. Client useBloc calls resolve against globalRegistry (the default the slot points at until something swaps it), exactly as in a non-SSR app.
That means the registries on the two sides are independent: the server’s per-request registry produced the HTML string, and the client’s globalRegistry rehydrates from scratch on mount. BlaC does not serialize the server registry into the client — blocs re-create from their constructors during hydration. If a bloc needs server-computed data to render identically on both sides, pass that data in as args (or serialize it into the page and feed it to the bloc’s init), the same way you would seed any client-side state for hydration.
Under Vite’s dev SSR (vite-node, ssrLoadModule) each request goes through the same module graph in one process, so the module-level registry slot is shared exactly as in production — the per-request pattern above applies unchanged in dev. The one extra hazard is HMR: hot-reloading the module that owns globalRegistry, or your ALS store, can leave you with two copies of the singleton (the old one and the freshly evaluated one) and instances split between them. If you see stale or duplicated instances only after a hot update, force a full reload rather than chasing it as a logic bug.