Strata architecture and SDDMenu
On this page

Architecture

Status: architectural constraints accepted; integrations and component mechanisms below are unproven until their roadmap gates pass. STATE owns implemented behavior and RESEARCH owns upstream observations.

Layers and ownership

Angular / Analog application
  ├─ simple API routes ───────────────────────────────┐
  ├─ internal RPC → Analog server functions           │
  └─ structured HTTP / server-only UI → Strata        │
                                          ↓          ↓
                                         H3 ← Nitro integration
                                          ↓
                                  Node / Cloudflare runtime

This is responsibility layering, not a claim that every call traverses a Strata runtime. Nitro builds and hosts the server; Analog owns application routing/rendering. Native HTTP and internal RPC remain available independently.

In the Server Components candidate, an explicit Vite registry may name a package selector, bare module and named export. The analyzer authorizes that reference only when the same named package import is present in the server-owned Angular component scope and the template uses [strataClient]. Package internals remain outside Strata's static composition walk. The surrogate imports the one approved export so Angular AOT can process the library; this does not establish automatic package trust or arbitrary package support. Qualification status is recorded in STATE and the package-island report.

PackageResponsibilityMust not own
@strata-sc/core — existsRuntime-independent controller/route metadata and future justified domain contractsH3 events, Angular injectors, Nitro lifecycle, Vite transforms, HTTP server
@strata-sc/h3 — existsRoute registration, HTTP execution; future H3 request/response/error mapping and version interoperabilityAngular compiler, app bootstrap, RPC duplication
@strata-sc/analog — exists (experimental)Concrete Analog registration, SSR/request lifecycle and build integration through supported extension pointsA replacement Analog router or an independent app provider registry
@strata-sc/compiler / @strata-sc/vite — candidatesGraph analysis/transformation versus Vite orchestration, only if those responsibilities warrant separate distributionGeneric framework runtime; packages created merely for symmetry
@strata-sc/angular / @strata-sc/testing — candidatesReusable Angular runtime boundaries / consumer testing helpers when demonstrated (SPEC-003 found none yet)A second DI container; re-export-only scaffolding

Adapter packages may depend on core, never the reverse. Angular-related dependencies belong in an integration boundary, not the existing metadata package. Decide dependency versus peer ranges when testing installed packages; today's dependency declarations are not a final release policy.

Package compilation, consumer compilation and declaration compatibility

These are distinct boundaries. The private TypeScript pre-transform builds/tests Strata packages; it is not shipped as a consumer compiler. SPEC-001 independently demonstrated consumer-written standard decorators through tsc → JavaScript → Vite SSR → Node, including metadata initialization and real H3 requests. Stock Vite 8.3.0 directly on that TypeScript retained decorator syntax and Node 22.23.0 rejected it. See measured results and review limits, including consumer Rolldown drift.

Consequently, consumers need an explicit lowering stage for this measured path. Neither a public @strata-sc/compiler/@strata-sc/vite package nor the safety of the private transform with Angular AOT has been established. Public compiler packaging remains open; M2 owns integration experiments.

Runtime evidence does not qualify declarations. The adapter's public H3 signature exposes H3's declaration graph: its Error.isError lib assumption and optional crossws import currently block strict consumers. Keep this requirement at the adapter/consumer compatibility boundary, never in core metadata or a decorator transform. A lib addition changes ambient type availability without polyfilling Node. Peer optionality at installation does not make a root declaration import optional. SPEC-002 tested a narrow published dependency closure and found none (upstream-blocked); no H3 major change, fork, ambient shim or permanent dependency policy is selected here. @strata-sc/analog's declarations do not reach H3 at all, so this boundary does not apply to it.

HTTP domain

H3 remains the HTTP authority. Future Strata request contracts should prefer Request, Response, Headers, URL and cancellation signals. H3-specific escape hatches belong to the H3 adapter; native handlers must continue to coexist without wrapping every endpoint in a controller.

Before HTTP API stabilization, one request-input spike must compare:

OptionWhat must be evaluated
Explicit handler context argumentDiscoverability, type inference, zero-argument compatibility, tests and runtime independence
Functional extractorsExplicit context ownership, params/query multiplicity, body consumption and composability
Injected request primitivesAngular request injector availability, synchronous resolution, lifetime and non-Angular use
Method/class decorator alternativesExplicit metadata without legacy parameter decorators; avoid encoding an entire schema DSL

Do not introduce @Param(), @Body() or @Query() parameter decorators. The input spec must decide decoding, missing values, repeated query keys, one-shot body reads, content types, size limits and Standard Schema sync/async validation before freezing signatures.

A later execution contract must define the order of context creation, authorization guards, validation, handler execution, interceptor unwind/error mapping and disposal, including short-circuiting and cancellation. Logging must carry request correlation without exporting secrets. Reuse H3 capabilities; do not invent an RxJS pipeline to obtain these semantics.

DI feasibility and ownership

Angular DI is the preferred mechanism, subject to proof. The spike must trace the exact Analog bootstrap, per-request injector construction and destruction for SSR and server-function dispatch, then establish whether a supported hook exists for controllers. Inspect pinned upstream source as well as public documentation; private access must be reported as a compatibility dependency.

The experiment compares a documented Analog injector hook, if available, with explicit Angular environment injectors owned by Strata's integration. Creating an independent injector must not silently produce a second application configuration or duplicate service instances.

Proof obligations:

  • Define process/application, render/request and controller instance lifetimes. Angular providedIn: 'root' alone does not establish a process-wide singleton in an SSR runtime.
  • Resolve dependencies during constructor/field initialization or synchronous invocation; retain resolved values across await. Do not promise post-await ambient inject().
  • Compare service identity across controller, SSR, server function and prospective server component in the same request; prove isolation across overlapping users/tenants and no retained request data.
  • Specify who destroys injectors on success, error, timeout and abort, and how tests override providers.
  • Test Node and Workers without assuming Node async-local storage is the solution.

@strata-sc/h3 still shares one controller instance across requests; that placeholder is never a safe location for request state. @strata-sc/analog creates a new controller instance per request through an experimental controllerFactory seam (default new Controller()) and runs the factory's onCleanup callbacks when the controller invocation settles. It provides no injector itself.

API boundary (opt-in). Routing stays owned by Nitro. registerControllers() can additionally register one fallback per owned prefix (/<prefix> and /<prefix>/**, every method) on the same router. Nitro's router always prefers a more specific route that handles the request's method, so the fallback only receives what nothing else handled; it answers a JSON 404, a 405 with Allow, a derived HEAD, or an opt-in plain OPTIONS, using route metadata scoped to that router in a WeakMap. It is not a router, adds no catch-all outside the declared prefixes and runs no guard or interceptor for requests that matched no route (report).

SPEC-003 result (CONDITIONAL GO, report). On Analog 2.7.2 / Angular 22.1.7, no supported seam exposes an Analog injector to Nitro handlers. The SSR injector exists only inside each renderApplication() call, the server-function injector is private to a generated module, and in production the SSR renderer inlines a separate copy of Angular. The selected model is an explicit host bridge:

  • The application owns Angular. Its Nitro plugin bootstraps an application injector with createApplication(config, { platformRef: platformServer() }), the way Analog bootstraps its server-function injector, and destroys it on Nitro close.
  • Per request, its controllerFactory creates a child createEnvironmentInjector() with the request-scoped providers and STRATA_REQUEST, constructs the controller in runInInjectionContext(), and hands destroy() to onCleanup.
  • @strata-sc/analog owns only the timing, and has no Angular dependency. StrataAnalogRequest stays the explicit HTTP input; inject() is valid only during construction (NG0203 in handlers, before or after await).

This is a separate DI universe: its providedIn: 'root' instances are not those of SSR renders or server functions. The Nitro bundle needs @angular/compiler declared in nitro.moduleSideEffects, because Nitro does not run the Angular linker. Local workerd builds the Worker graph but refuses Angular JIT factory compilation with Code generation from strings disallowed; consumer-owned Angular DI is therefore NO-GO on the checked Worker tuple. Provider overrides and abort/timeout remain open; a streamed body keeps the request scope open only through the opt-in streamResponse() (0.7.0 evidence). Guard/interceptor composition with this DI fixture is also not qualified. No DI public API, package or ADR is selected; the glue stays consumer code until Server Components show a reusable responsibility. See the cross-runtime qualification.

Strict Server Component boundary

SSR means a component renders on the server; its implementation may also ship to the browser. A Server Component means its implementation belongs exclusively to the server graph. The provisional name @ServerComponent expresses intent, not a committed decorator signature.

ModuleServer graphBrowser graph
ProductDetails server implementation, including compiled server template logicRequiredForbidden
DatabaseService, ORM and transitive server dependenciesRequired when usedForbidden
AddToCartComponent normal Angular interactive boundaryMay render on server and be referenced as a boundaryRequired
Explicit serializable public props / safe boundary identityProduced by serverAllowed

The browser may receive rendered HTML and a separate boundary representation. A generated client surrogate, if needed, must not contain or import the server implementation. Type-only references must erase; runtime imports and re-exports must be checked transitively. Templates, lazy chunks, public source maps and preload manifests count as browser output.

hydrate never, isPlatformServer, filename conventions and tree shaking alone cannot establish this guarantee. The compiler experiment must show how Angular's compiled view/hydration metadata can retain an interactive descendant without shipping its server parent. Failure to find a supported approach is a valid no-go result; CSR recreation is not equivalent to successful hydration.

Current experiment (not a committed design). The private, unpublished @strata-sc/server-components package holds the mechanism the PoCs qualified: a build-time @ServerComponent() marker, a Vite transform that swaps the module for an empty-template surrogate in the client environment only (and fails the build if the real module reaches it), an explicit [strataClient] boundary carrying plain-data props, and an island host that hydrates each boundary as its own root from its ngh annotation. Client references are derived from the elements marked [strataClient] in the template. It supports document navigation only and relies on undocumented Angular hydration behaviour; see its README and the graph, navigation and Cloudflare reports.

Module identity (0.6 candidate). A forbidden module is identified by its resolved file path with the query removed, never by a specifier. The pre-scan resolves re-export specifiers through relative paths, index files, Vite resolve.alias and tsconfig paths into the scanned file set only (no node_modules crawl); the client environment's resolveId checks what Vite resolved for path and aliased specifiers; and load checks every module the client loads, which is the backstop for forms the pre-scan does not model. Packages are enforced through their server-only assertion import. Diagnostics carry the importer and import chain the bundler reports, sanitized to app-relative paths. See the report.

Boundary data and security

The eventual serialization contract must enumerate permitted values, encoding/versioning, size limits and rejection behavior. Start evaluation with explicit plain-data props; do not serialize service instances, closures, arbitrary class prototypes, credentials or provider graphs. Rich values such as dates and big integers require an explicit tested decision.

Graph exclusion prevents code delivery; it does not prevent leaking data through HTML, props, transfer state, error bodies or logs. Tests must cover both. Request authorization also applies to direct subtree/payload requests if such endpoints exist. User-specific responses and cached payloads must be isolated by identity/tenant and invalidation policy. Client-supplied boundary identifiers must not resolve arbitrary modules or invoke arbitrary server code.

Before 1.0 define safe error output for initial render failures, failure after headers/streaming begin, serialization rejection, missing/stale boundary IDs, navigation failure and hydration mismatch. If streaming is not supported, document and test buffered rendering; do not imply a streaming protocol. Production failures must not expose stacks, source paths or secrets.

Initial SSR is the first proof, not a complete rendering architecture. M2 identifies viability; M4 chooses and specifies one production navigation strategy using measured fixtures:

CandidateRequired proofCost to evaluate
Full document navigation for server-component routesLinks, programmatic navigation, redirects, deep links and history consistently request server documents; interactive boundaries hydrate on each loadState reset, transition latency, scroll/focus and mixed CSR routes
Client router plus server subtree/payloadStable payload identity/versioning, Angular-supported attach/hydrate lifecycle, cleanup, cancellation, history and error recoveryFramework coupling, cache/auth/serialization surface and retained state

The selected path is full document navigation, exposed by the optional @strata-sc/server-components/navigation entry as a native-anchor directive, an explicit navigator service and a route guard that share one same-origin URL resolver built on Angular's public Router, UrlSerializer and Location (report). It does not replace the Angular Router and does not add SPA subtree hydration.

Document navigation is eligible for 1.0 only if explicitly selected, automated and acceptable to the real consumer. It is not an excuse to leave normal router links rendering missing server code. Do not copy React Flight, invent Suspense, inject arbitrary DOM into Angular-owned views or ship the server component as a CSR fallback. If neither strategy is safe, 1.0 remains blocked.

Test ordinary hydration, incremental hydration, nested @defer, event replay, projected/nested interactive children and mixed conventional routes. Enumerate unsupported compositions with clear build diagnostics. No public boundary API is frozen before those constraints are understood.

Source: docs/ARCHITECTURE.md

Generated by Entwine · Markdown remains the source of truth.