Server Components and structured server APIs for Angular and Analog.
Strata makes server-side Angular a coherent application model, on two pillars:
- Server Components — run Angular components on the server without shipping
their implementation or server dependencies to the browser. Only children
marked as explicit client boundaries (
[strataClient]) hydrate, as interactive islands. Experimental:@strata-sc/server-componentsis published on npm as0.x(API may change before 1.0); it is dogfooded on the official website and qualified in this repository on Node/Nitro and Cloudflare workerd, for initial/document navigation only (Angular Router SPA navigation into a new Server Component subtree is not supported yet). See its README. - Controllers — declare HTTP APIs with standard-decorator controllers
(
@Get,@Post,@Put,@Patch,@Delete) and register them on the Nitro/H3 router you already own.@strata-sc/coreand@strata-sc/analogare published on npm as0.x.
Both leave Angular, Analog and Nitro in control of their runtime. Strata is experimental, intentionally narrow, and designed in public.
Strata takes its name from stratum / strata — layers. The goal is a layered server architecture for Angular and Analog: server-rendered UI with explicit client islands, and structured HTTP APIs, on the Nitro / H3 runtime.
Experimental, pre-1.0, and developed in public. Strata stays deliberately small: a build-time server/browser graph split for Server Components, and controller metadata with runtime adapters, without taking control of the H3 or Nitro application you already own.
Technical documentation lives as Markdown in docs/, starting with
Getting started. It is compiled by
Entwine into a separate static site:
https://andersseen.github.io/Strata/.
apps/wwwremains the official Analog site, deployed to Cloudflare Pages. The Entwine site is independent of it.- The documentation is the committed Markdown; nothing is generated from source code. Pushes to
mainrebuild it throughentwine.yml; pull requests only validate it. - Entwine's read-only MCP server and three agent skills help coding agents read and maintain the
docs. The skills are installed under
.agents/skills/and.claude/skills/; the MCP package is not on npm yet. Agent instructions and skills are never published to the site.
Local commands (the CLI version is pinned to the one the workflow uses):
npx --yes @entwine/cli@0.5.1 check # validate documentation
npx --yes @entwine/cli@0.5.1 build # generate dist/
npx --yes @entwine/cli@0.5.1 dev # preview locallyMCP registration (needs a local build, not yet installable from npm), skill reinstallation and regenerating the workflow after an Entwine upgrade are in the documentation workflow.
The repository includes the official Strata landing page at
apps/www. It is an SSR-enabled AnalogJS
application built with Angular, Tailwind CSS 4,
@voltui/components,
Angular Movement, and
Lumen Icons. Its homepage
dogfoods Server Components: the "Server Components" section is a real
@ServerComponent() that injects a server-only repository (which imports a
further server-only module) and renders one interactive client island. It also
has a light/dark theme switcher, Server Component, controller and Analog
examples, and an explanation of Strata's runtime boundary.
pnpm test:www:server-components qualifies that dogfood on the site's
production builds: a plain-SSR control build, the browser/server graph split
(markers and per-chunk source modules), HTTP SSR, and the site's Playwright
suite (hydration, interaction, no Angular hydration errors) on Nitro
node-server and on the Cloudflare Pages build under local wrangler pages dev.
pnpm --filter @strata-sc/www devCreate a production build with:
pnpm --filter @strata-sc/www buildapps/relay is a private, local-only operations console used to exercise Strata in
a realistic Analog application. It is deliberately separate from the official website and has no
deployment configuration.
Relay uses Strata controllers for an operations snapshot, service details, incident creation and incident transitions. The flows cover route params, query strings, request bodies, headers, a request-scoped controller factory and cleanup, while a native Analog health route remains alongside them. Its Angular UI uses Volt UI for the dashboard, service inventory and incident workflow.
It also contains two practical Server Component examples:
-
The main dashboard renders a server-only operations briefing with two independent interactive client boundaries and flat serialized props.
-
/releaserenders a server-only release assessment plus an interactive rollout simulator. Links to and from this route deliberately use full document navigation, the only navigation mode the current experiment qualifies. -
/incidentsrenders a live incident digest from the same in-memory repository the Strata controllers write to, with one triage island per incident (inside@for) that mutates throughPATCH /api/ops/incidents/:id; the next document request renders the new state. The repository is a process-wide singleton because Analog bundles the Angular SSR code separately from Nitro's server code, so a plain module-level instance would exist twice.
pnpm relay:verify:server-components (the same gate as pnpm test:relay:server-components)
checks a plain-SSR control build, the browser/server graph split (markers and per-chunk source
modules), each route's SSR, a controller write the next server render must reflect, and Relay's
Playwright suite (hydration of every island, interaction, no Angular errors).
pnpm relay:devRun its focused checks with:
pnpm relay:test
pnpm relay:build
pnpm relay:verify:server-components
pnpm relay:test:e2eThe official site is configured for direct upload to the strata-www
Cloudflare Pages project. Create that project once (after authenticating
Wrangler), then use the root scripts to build, run the Pages runtime locally,
or make a production deployment:
pnpm www:pages:create
pnpm www:pages:dev
pnpm www:pages:deploywww:pages:dev builds with the cloudflare-pages Nitro preset and runs the
result through wrangler pages dev at http://localhost:8789, so the local
runtime matches Pages rather than the default Node preview. GitHub Actions deploys the same artifact only
for pushes to main after CI passes. Add CLOUDFLARE_API_TOKEN (with Pages
write permission) and CLOUDFLARE_ACCOUNT_ID as repository secrets before
the first merged deployment.
Registry (experimental). @strata-sc/core, @strata-sc/analog and
@strata-sc/server-components are published to npm. 0.x versions are pre-1.0
and may break in any minor:
pnpm add @strata-sc/core @strata-sc/analogStable releases are on the latest dist-tag; release candidates go to next
(ADR-005). @strata-sc/h3 is not
published to npm yet (see SPEC-002
and ADR-004).
Source (main). To work on Strata itself, clone the repository and use the
pnpm workspace (pnpm install && pnpm build). Workspace packages are linked
with workspace:*. External projects should install the registry packages,
not link to a checkout.
Releases are automated with Changesets and the Release workflow:
- Every PR that changes a package adds a changeset (
pnpm changeset) choosingpatch,minorormajorfor each package it touches. - On merge to
main, the workflow opens or updates a Version Packages PR with the bumped versions and CHANGELOGs. - Merging that PR publishes the changed packages to npm on the
latestdist-tag, aligns the dist-tags, and creates the matching git tags and GitHub Releases, but only after the aggregate CIVerifyjob succeeded for that exact commit and the commit is still the currentmainhead. The workflow checks this before starting and again immediately before Changesets runs.
While packages are 0.x, breaking changes are released as minor. Release
candidates use Changesets pre mode (pnpm changeset pre enter next) and are
published on next without touching latest.
@strata-sc/h3 is private and is not published.
See GitHub releases and all tags for published history.
This repository is still in early development. @strata-sc/core has a first
experimental API for declaring controllers and routes, built on standard
ECMAScript decorators, and @strata-sc/h3 now wires that metadata into a real
H3 app. @strata-sc/analog registers the same controllers inside an
Analog 2 app. Everything here is experimental and pre-1.0. @strata-sc/core and
@strata-sc/analog are published to npm as 0.x.
import { H3 } from "h3";
import { Controller, Get } from "@strata-sc/core";
import { registerControllers } from "@strata-sc/h3";
@Controller("/users")
class UsersController {
@Get()
findAll() {
return [{ id: "1" }];
}
}
const app = new H3();
registerControllers(app, [UsersController]);@Controller and @Get only build declarative metadata describing a
controller's routes. @strata-sc/h3 is a thin adapter on top of that metadata:
it reads each controller's definition via @strata-sc/core's public
getControllerDefinition API, registers its @Get() routes directly on the
H3 app you pass in, and invokes the matching controller method when H3
resolves a route. Strata does not replace H3 or own the HTTP runtime — you
still create and control the H3 instance yourself, and any routes you
register on it directly keep working unchanged.
There is no dependency injection or controller lifecycle yet: @strata-sc/h3
instantiates each controller once, with new ControllerClass(), at
registration time. This is a deliberately minimal, provisional placeholder
until a real lifecycle/DI design lands in a later iteration — it is not a
stable API.
Analog 2 runs on Nitro 2, which runs on H3 v1 — a different major from the H3 v2 that
@strata-sc/h3 targets — so Analog uses a separate, sibling adapter, @strata-sc/analog. It registers
controllers on the router Nitro already owns (nitroApp.router, part of Nitro's public plugin
API), from a Nitro server plugin. It does not depend on @strata-sc/h3, and it does not depend on
h3, nitropack or @analogjs/* either.
// src/server/strata/users.controller.ts
import { Controller, Get } from "@strata-sc/core";
import type { StrataAnalogRequest } from "@strata-sc/analog";
@Controller("/api/strata/users")
export class UsersController {
@Get()
findAll() {
return [{ id: "1", name: "Ada" }];
}
@Get("/:id")
findOne(request: StrataAnalogRequest) {
return { id: request.params["id"], name: "Ada" };
}
}// src/server/plugins/strata.ts
import { registerControllers } from "@strata-sc/analog";
import { defineNitroPlugin } from "nitropack/runtime";
import { UsersController } from "../strata/users.controller";
export default defineNitroPlugin((nitroApp) => {
registerControllers(nitroApp.router, [UsersController]);
});GET /api/strata/users answers 200 JSON in analog dev and in the production server, alongside
native Analog routes.
Controllers are request-scoped (experimental): registerControllers() only reads and validates
metadata at startup, and every matching request gets a new controller instance, created with
new UsersController() by default. Instances are never cached, pooled or shared between requests,
so instance fields cannot leak state across users. A custom controllerFactory (sync or async) can
take part in creating each request's controller; it receives the controller class and the request's
StrataAnalogRequest, and must return an instance of that class:
const greetings = new GreetingService();
registerControllers(nitroApp.router, [GreetingController], {
controllerFactory: (Controller) =>
Controller === GreetingController ? new GreetingController(greetings) : new Controller(),
});The factory can also release what it creates for a request (experimental). onCleanup registers a
callback, sync or async, that Strata runs when the controller invocation finishes, including error
paths:
registerControllers(nitroApp.router, [UsersController], {
controllerFactory: (Controller, { onCleanup }) => {
const resource = createResource();
onCleanup(() => resource.dispose());
return new Controller();
},
});Every registered cleanup runs exactly once, after the factory and the awaited handler call settle,
whether they succeed, the factory throws or rejects, or the handler throws or rejects. Cleanups run
last registered first (LIFO), each awaited before the next, and the request's response is sent only
after all of them. None of the errors is hidden: after trying every cleanup, Strata rethrows the
original error, or the single cleanup error if the invocation succeeded, and otherwise an
AggregateError with the original error (if any) followed by every cleanup error. Cleanup covers
the controller invocation, not a streamed response body.
The factory is an extension seam, not a DI container: Strata provides no injector. Errors it throws
or rejects with propagate to Nitro unchanged; a result that is not an instance of the controller
class fails with StrataAnalogConfigurationError. (@strata-sc/h3 still creates one instance per
controller at registration.)
Angular DI works through this seam, with injectors the application owns. This is an experimental
result, not a Strata API (see SPEC-003).
The fixture's Nitro plugin bootstraps an Angular application injector once. Its factory builds each
controller inside a per-request child injector and destroys that injector with onCleanup, so
controllers use ordinary inject():
@Controller("/api/strata/angular-di")
export class CatalogController {
private readonly catalog = inject(CatalogService); // application lifetime
private readonly identity = inject(RequestIdentity); // request lifetime
@Get("/products/:id")
async findOne(request: StrataAnalogRequest) {
await somethingAsync();
return this.catalog.findProduct(request.params["id"] ?? "");
}
}Resolve dependencies at construction and keep them across await: calling inject() inside a
handler throws NG0203. The injectors are separate from Analog's SSR and server-function
injectors, which no supported Analog seam exposes, so providedIn: 'root' services are not shared
with them. The Nitro bundle also needs @angular/compiler in nitro.moduleSideEffects. See the
feasibility report for the evidence and limits.
Handlers may accept one provisional StrataAnalogRequest
argument for params, query, headers, URL/path, context and lazy body readers; Nitro's H3 event is
not exposed to controller code. Controllers under src/server/** stay out of the client bundle. The
integration report records what is verified
(Analog 2.7.2, Nitro 2.13.4) and what is not.
Nothing here should be considered stable — the API can still change in
breaking ways in any 0.x release.
The architecture and SDD documentation records the current
implementation, decisions, risks and roadmap to 1.0.
Work runs on two tracks. H3 consumer qualification: packed-consumer runtime verification has
executed, but strict consumer types for @strata-sc/h3 are upstream-blocked by H3/crossws declarations
(SPEC-002), so M1 stays blocked. Analog
integration: @strata-sc/analog has request input, per-request controllers, request cleanup and a
conditional go for Angular DI (SPEC-003).
Strict Server Component PoCs reached a conditional go on Node and on local Cloudflare workerd, for
document navigation only; Angular Router navigation is a recorded no-go. Their mechanism lives in the
experimental @strata-sc/server-components package (published as 0.x). Analog integration is still limited to
GET controllers on the Nitro router, and Strata will not use legacy parameter decorators.
Strata 1.0 requires production-ready Server Components in a real Angular/Analog application: server implementations and dependencies excluded from browser output, with explicit interactive Angular descendants and a tested navigation strategy. Only an experimental, unpublished mechanism exists so far (evidence); the capability is not production-ready. See the 1.0 release gates.
| Package | Description |
|---|---|
@strata-sc/core |
Experimental @Controller / @Get metadata primitive. |
@strata-sc/h3 |
Experimental H3 adapter: registers Strata controllers on an H3 app. |
@strata-sc/analog |
Experimental Analog adapter: registers Strata controllers on the Nitro router. |
@strata-sc/server-components |
Experimental (0.x). Server Component graph split: marker, client boundary, island runtime and Vite transform. |
- TypeScript (strict, ESM)
- pnpm workspaces
- Turborepo
- Vite (library mode)
- Vitest
- ESLint (flat config)
- Prettier
- Changesets
The packages target Node.js >= 22. Developing this repository requires Node.js >= 22.22.3,
the floor of the Angular 22 / Analog 2.7 toolchain used by the fixture (apps/analog-fixture);
that requirement is not imposed on the published packages.
See CONTRIBUTING.md. Community expectations are in the Code of Conduct, security reports follow SECURITY.md, and support routes are collected in SUPPORT.md.