Menu
On this page

Setu-TS — Architecture Guide

This document is the definitive technical architecture guide for framework contributors. It explains how the framework works internally and why it was designed this way.

Audience: Framework contributors and maintainers, not application developers.

Companion documents:


Table of Contents

  1. Vision
  2. Design Principles
  3. High-Level Architecture
  4. Request Lifecycle
  5. Plugin Architecture
  6. Service Registry
  7. Runtime Layer
  8. Package Architecture
  9. Dependency Rules
  10. Middleware Pipeline
  11. Dependency Injection
  12. Decorator System
  13. Error Handling
  14. Security Model
  15. Performance Philosophy
  16. Testing Strategy
  17. Extending the Framework
  18. Future Evolution
  19. Deployment
  20. Conclusion

1. Vision

Why This Framework Exists

Enterprise backend development faces a recurring tension: frameworks that are powerful tend to be heavy and opinionated, while frameworks that are lightweight tend to lack enterprise features.

NestJS provides excellent developer experience with decorators, DI, and modules, but it is tightly coupled to Node.js, requires reflection, and has a steep learning curve. Fastify is fast and plugin-oriented but lacks the enterprise abstractions (repository pattern, CQRS, multi-tenancy, audit logging) that large organizations need. Hono delivers exceptional performance and runtime portability but is intentionally minimal — it provides routing, not architecture.

Setu-TS exists to bridge this gap. It combines:

SourceWhat We Take
HonoPerformance, runtime portability, routing engine
Spring BootPlugin auto-configuration, conditional registration, starter bundles
ASP.NET CoreMiddleware pipeline, request/response abstractions
FastifyPlugin encapsulation, decoration pattern, lifecycle hooks

Problems It Solves

  1. Runtime lock-in — NestJS, Express, and Fastify are Node.js-first. Setu-TS runs on Node.js, Deno, and Bun with zero code changes, and is designed for Cloudflare Workers extensibility.

  2. Opinionated heaviness — NestJS requires DI, decorators, and reflection. Setu-TS makes all of these optional. Start with just a router and add capabilities as needed.

  3. Monolithic frameworks — Most frameworks bundle all features. Setu-TS is plugin-first: every capability (logging, database, auth, validation) is a plugin that can be added, removed, or replaced.

  4. Poor replaceability — In most frameworks, swapping the database layer or logger requires touching application code. Setu-TS uses capability tokens, so any plugin can be replaced without modifying business logic.

  5. Enterprise feature gaps — Lightweight frameworks lack secrets management, audit logging, circuit breakers, feature flags, and multi-tenancy. Setu-TS provides these as plugins.

Why It Is Not a NestJS Clone

NestJS is built around three mandatory pillars: modules, dependency injection, and decorators. Every NestJS application must use all three. This creates a steep learning curve and tight coupling to the framework’s opinions.

Setu-TS inverts this:

AspectNestJSSetu-TS
DIRequiredOptional plugin
DecoratorsRequiredOptional plugin
ReflectionRequiredOptional
ModulesRequiredPlugins (more granular)
Programmatic APILimitedFull API for everything
ReplaceabilityDifficultAny plugin swappable
RuntimeNode.js onlyNode, Deno, Bun, CF Workers
Bundle sizeLargePay only for what you use

Runtime vs build toolchain. The Runtime row describes where the framework runs. The React Router SSR plugin (M44) is web-standard at runtime — it runs wherever Request/Response do — but its frontend build uses the Node/npm toolchain (Vite) outside the Deno workspace, and is never a dependency of a JSR-published package (see AI_GUIDELINES §12.2). CF Workers support for React Router framework mode specifically carries additional constraints and is not implied by this row.

A Setu-TS application can be as minimal as:

const app = createApplication({ plugins: [RuntimePlugin()] });
app.router.get('/', (ctx) => ctx.response.json({ hello: 'world' }));
await app.start();

No decorators, no DI, no modules — just a router and a runtime. Capabilities are added incrementally via plugins.

Why It Uses Hono

As of Milestone 22, Hono is used as the internal routing engine inside the kernel Router class. Route matching is delegated to a new Hono({ strict: false }) instance constructed with Hono’s TrieRouter (M22 chose LinearRouter; TrieRouter replaced it in 0.11.0 because it returns the same full candidate set without re-parsing every pattern per request). The kernel calls the router directly, so it applies LinearRouter’s trailing-slash rule itself: a request may carry one extra trailing slash (/users/ reaches /users), a static route registered with a trailing slash answers only that spelling, and a parameter pattern is registered without its trailing slash, which was never significant. For a slash-terminated request it matches both spellings and ranks the candidates together. The kernel’s custom (ctx, next) middleware pipeline runs inside the Hono dispatch, and the application’s #handleRequest method calls router.match() which maps Hono’s matched route back to { definition, params }. The kernel maintains its own RouteEntry map so getAll()/listRoutes() introspection is unchanged, and the kernel applies its own deterministic tie-break on top of Hono’s match result: more static segments first, then FEWER * wildcard segments, then earliest registration order. The wildcard key is what stops an application catch-all (GET /*) from owning every single-segment route registered after it — before M70g a * counted as a static segment, so /* tied with /openapi.json and won merely by registering first. The rule compares counts rather than positions, so /a/* loses to /:x/b on /a/b; that limit is documented in Router.match and pinned by a test.

listRoutes() and the kernel-diagnostics route projection are two DIFFERENT surfaces and neither replaces the other. RouteInfo.definition carries the live handler, its middleware functions and its schema objects: it is an in-process contract for plugins that must call or wrap a route (openapi-plugin reads the Zod schemas; M70g reads owner for the duplicate-route refusal), and it is deliberately NOT wire-safe. The diagnostics projection added in Milestone 98a selects its own bounded primitives — an opaque id, the HTTP verb, and the registered pattern only when the application allowlisted that exact pattern — and can therefore leave the process. M98a widened neither IRouterApi.listRoutes() nor RouteInfo; the projection is captured at the router’s own registration boundary through an internal sink.

Hono is chosen for several reasons:

  1. Performance — Hono is one of the fastest TypeScript routers, competitive with Fastify.
  2. Runtime portability — Hono already runs on Node.js, Deno, Bun, and Cloudflare Workers.
  3. Minimal — Hono does not impose an architecture; it provides routing and lets the framework build on top.
  4. Standards-based — Hono uses the standard Request/Response Web APIs.

Setu-TS does not expose Hono directly to application developers. Instead, it wraps Hono in the kernel router; the runtime adapter (M41 today, M23 next) handles only socket serving. This allows the framework to swap Hono for another router in the future without breaking applications.

Why It Is Plugin-First

A plugin-first architecture provides three critical benefits:

  1. Pay only for what you use — If you do not need a database, the database plugin is never loaded. No dead code, no unused dependencies, no startup overhead.

  2. Replace anything — Every capability is identified by a capability token (a string). If you do not like the default logger, you register a different plugin that provides the same logger token. Application code does not change.

  3. Extensible without forking — Need a feature the framework does not provide? Write a plugin. No need to fork the framework or monkey-patch internals.

Why Runtime Independence Matters

The JavaScript ecosystem is fragmenting across runtimes:

  • Node.js — The incumbent, with the largest ecosystem.
  • Deno — Secure by default, native TypeScript, web-standard APIs.
  • Bun — Extremely fast, Node-compatible, native TypeScript.
  • Cloudflare Workers — Edge computing, V8 isolates, no Node.js APIs.

Applications written today on Node.js may need to run on Bun tomorrow for performance, or on Cloudflare Workers for edge deployment. Setu-TS ensures that business logic never depends on runtime-specific APIs. All runtime-specific operations (UUID generation, timers, crypto, file system, environment variables) are abstracted behind IRuntimeServices and provided by the RuntimePlugin.


2. Design Principles

Plugin-First Architecture

Every capability is a plugin. The kernel ships with zero features beyond plugin orchestration. This means:

  • The kernel is small and stable.
  • Features are independently versioned.
  • Applications include only what they need.
  • Any feature can be replaced or removed.

Composition Over Inheritance

The framework avoids deep inheritance hierarchies. Instead:

  • Exceptions are created via factory functions, not class inheritance.
  • Plugins compose via capability tokens, not abstract base classes.
  • Middleware composes via pipeline execution, not middleware classes inheriting from a base.
  • Services are composed via the service registry, not via class hierarchies.

Dependency Inversion

All dependencies point toward abstractions:

  • Plugins depend on interfaces from @setu-ts/common.
  • Services are resolved by capability token, not by class reference.
  • Adapters implement interfaces defined in common.
  • No package depends on a concrete implementation from another package.

Runtime Abstraction

No package except @setu-ts/runtime may use runtime-specific APIs. This rule is enforced by:

  • Lint rules (custom deno lint plugin rule / CI gate) that forbid node: and bun: imports and Deno.* globals outside the runtime package.
  • Code review.
  • CI tests that run on all three runtimes.

Capability Tokens

Plugins communicate via capability tokens — strings that identify a capability, not a concrete type:

// Plugin A provides the 'database' capability
ctx.services.register('database', new PrismaDatabaseService());

// Plugin B consumes the 'database' capability
const db = ctx.services.get<IDatabaseService>('database');

This decouples plugins from each other. Plugin B does not know that Plugin A provides the database service — it only knows that something provides the database capability. This enables:

  • Replaceability — Swap Prisma for Drizzle by registering a different plugin.
  • Testing — Register a mock service with the same token.
  • Decoupling — Plugins never import from each other.

Programmatic APIs

Every feature has a complete programmatic API. No feature requires decorators, DI, or reflection. This ensures:

  • The framework works in environments that do not support decorators (e.g., some bundlers strip them).
  • Developers who prefer functional style are not forced into OOP.
  • Testing is easier — no need to bootstrap a decorator system.

Optional Decorators

Decorators are provided by the DecoratorPlugin. When registered, it reads metadata stored by decorators and registers routes, services, and middleware with the kernel. When not registered, decorators are inert — they store metadata that is never read.

Optional Dependency Injection

DI is provided by the DiPlugin. When registered, it provides a container for resolving services by token. When not registered, services are resolved directly from the ServiceRegistry. The ServiceRegistry is the primary service resolution mechanism; the DI container is a convenience layer on top.

Tree Shaking

Every package uses:

  • ES module exports only.
  • Subpath exports for granular imports.
  • No top-level side effects.

This ensures bundlers can eliminate unused code.

SOLID

  • S — Each plugin has one responsibility.
  • O — Plugins extend behavior via registration, not modification.
  • L — Any adapter implementing an interface is substitutable.
  • I — Interfaces are segregated (e.g., IReadRepository vs IWriteRepository).
  • D — All dependencies point to interfaces in common.

Clean Architecture

Dependency direction always points inward:

Plugins (Infrastructure) → Application Code → Domain Code
                                ↑
                    depends on interfaces only

Hexagonal Architecture

  • Ports — Interfaces in @setu-ts/common (e.g., IDatabaseService, ICacheStore, IMessageBroker).
  • Adapters — Implementations in plugins (e.g., PrismaAdapter, RedisStore, RabbitMqBroker).
  • Application Core — Business logic depends only on ports.

3. High-Level Architecture

graph TB
    subgraph Application Layer
        App[Application Instance]
    end

    subgraph Kernel
        Kernel[Plugin Kernel]
        Registry[Plugin Registry]
        Services[Service Registry]
        Pipeline[Middleware Pipeline]
        Router[Router]
    end

    subgraph Plugins
        Runtime[RuntimePlugin]
        Logger[LoggerPlugin]
        Config[ConfigPlugin]
        Validation[ValidationPlugin]
        Database[DatabasePlugin]
        Auth[AuthPlugin]
        Other[...Other Plugins]
    end

    subgraph Runtime Adapters
        Node[Node Adapter]
        Deno[Deno Adapter]
        Bun[Bun Adapter]
    end

    App --> Kernel
    Kernel --> Registry
    Kernel --> Services
    Kernel --> Pipeline
    Kernel --> Router

    Registry --> Runtime
    Registry --> Logger
    Registry --> Config
    Registry --> Validation
    Registry --> Database
    Registry --> Auth
    Registry --> Other

    Runtime --> Node
    Runtime --> Deno
    Runtime --> Bun

    Pipeline -.-> Router
    Router -.-> Pipeline

Component Responsibilities

ComponentResponsibility
ApplicationEntry point for developers. Holds plugin list, starts/stops server, provides inject() for testing.
Plugin KernelOrchestrates plugin registration, lifecycle, and shutdown. The only component that knows about all plugins.
Plugin RegistryStores registered plugins, resolves dependencies, detects cycles, orders by priority.
Service RegistryMaps capability tokens to service instances. The primary service resolution mechanism.
Middleware PipelineExecutes middleware in order. Created by the kernel, populated by plugins.
RouterRegisters and matches routes. Populated by plugins and application code.
PluginsProvide capabilities (logging, database, auth, etc.) by registering services, middleware, routes, and hooks.
Runtime AdaptersAbstract runtime-specific APIs (HTTP server, UUID, timers, crypto, fs).

Key Insight

The kernel knows nothing about what the application does. It only knows how to:

  1. Register plugins.
  2. Resolve plugin dependencies.
  3. Build the middleware pipeline.
  4. Build the router.
  5. Start and stop the HTTP server (delegated to the runtime adapter).

All domain knowledge lives in plugins. This is what makes the framework replaceable and extensible.


4. Request Lifecycle

sequenceDiagram
    participant Client
    participant Runtime as Runtime Adapter
    participant Kernel
    participant Pipeline as Middleware Pipeline
    participant Router
    participant Handler as Route Handler
    participant Services as Service Registry

    Client->>Runtime: HTTP Request
    Runtime->>Kernel: Normalized Request
    Kernel->>Pipeline: Create RequestContext
    Pipeline->>Pipeline: Execute middleware chain

    Note over Pipeline: 1. LoggingMiddleware
    Note over Pipeline: 2. RequestIdMiddleware
    Note over Pipeline: 3. CorsMiddleware
    Note over Pipeline: 4. SecurityHeadersMiddleware
    Note over Pipeline: 5. AuthMiddleware
    Note over Pipeline: 6. ValidationMiddleware

    Pipeline->>Router: Match route
    Router->>Router: Extract params
    Router->>Handler: Call handler with context

    Handler->>Services: Resolve services
    Services-->>Handler: Return service instances
    Handler->>Handler: Execute business logic
    Handler-->>Router: Return response

    Router-->>Pipeline: Response
    Pipeline->>Pipeline: Execute response middleware
    Note over Pipeline: 7. ResponseInterceptors
    Note over Pipeline: 8. MetricsMiddleware
    Note over Pipeline: 9. ErrorHandler (if error)

    Pipeline-->>Kernel: Final response
    Kernel-->>Runtime: Send response
    Runtime-->>Client: HTTP Response

Detailed Flow

  1. Incoming Request — A client sends an HTTP request to the server.

  2. Runtime Adapter — The runtime adapter (Node, Deno, or Bun) receives the raw HTTP request and converts it to the framework’s IRequest abstraction. This normalizes the request across runtimes.

  3. Kernel — The kernel receives the normalized request and creates a RequestContext. The context provides access to:

    • The request and response objects.
    • The service registry (for resolving services).
    • A state map (for passing data between middleware).
    • The request ID and correlation ID.
  4. Middleware Pipeline — The kernel passes the context through the middleware pipeline. Middleware executes in priority order:

    • Logging — Logs the incoming request.
    • Request ID — Generates or propagates a unique request ID.
    • CORS — Handles preflight requests.
    • Security Headers — Adds security headers to the response.
    • Authentication — Extracts and validates credentials, populates ctx.request.user.
    • Authorization — Checks roles and permissions.
    • Validation — Validates the request body, query, params, headers against Zod schemas.
  5. Router — After middleware, the pipeline calls the router to match the request path and method to a registered route. The router extracts path parameters.

  6. Route Handler — The matched route’s handler is called with the context. The handler:

    • Resolves services from the service registry via ctx.services.get<T>(token).
    • Executes business logic (calling services, repositories, etc.).
    • Returns a response via ctx.response.json(), ctx.response.send(), etc.
  7. Response Middleware — After the handler returns, the pipeline executes response-phase middleware:

    • Response Interceptors — Transform the response (e.g., wrap in a standard envelope).
    • Metrics — Record request duration and status code.
    • Error Handler — If any middleware or handler threw an error, the error handler catches it and formats a standardized error response.
  8. Response — The kernel sends the final response back through the runtime adapter to the client.

Short-Circuiting

Any middleware can short-circuit the pipeline by sending a response without calling next(). For example, the auth middleware can return a 401 response if authentication fails, and the pipeline stops — the router and handler are never called.

Error Propagation

If any middleware or handler throws an error, the pipeline catches it and passes it to the error handler middleware. The error handler:

  1. Logs the error (if a logger is available).
  2. Formats the error as a standardized response (RFC 9457 Problem Details when configured).
  3. Sends the error response to the client.

5. Plugin Architecture

Plugin Lifecycle

stateDiagram-v2
    [*] --> Discovered
    Discovered --> Sorted: Dependencies resolved
    Sorted --> Registered: register() called
    Registered --> Initialized: onInit hooks
    Initialized --> Bootstrapped: onBootstrap hooks
    Bootstrapped --> Active: Server listening
    Active --> ShuttingDown: Shutdown signal
    ShuttingDown --> Shutdown: onShutdown hooks
    Shutdown --> Closed: onClose hooks
    Closed --> [*]

Plugin Registration Flow

sequenceDiagram
    participant App
    participant Kernel
    participant Registry as Plugin Registry
    participant Services as Service Registry
    participant Pipeline
    participant Router

    App->>Kernel: register(plugin)
    Kernel->>Registry: add(plugin)
    Registry->>Registry: Store plugin metadata

    Note over Kernel: When app.start() is called:

    Kernel->>Registry: Resolve dependencies
    Registry->>Registry: Topological sort
    Registry->>Registry: Detect cycles
    Registry-->>Kernel: Ordered plugin list

    loop For each plugin in order
        Kernel->>Registry: plugin.register(ctx)
        Registry->>Services: Register services
        Registry->>Pipeline: Add middleware
        Registry->>Router: Add routes
        Registry->>Services: Register health checks
        Registry->>Services: Register metrics
        Registry->>Services: Register CLI commands
    end

    Kernel->>Pipeline: Build pipeline
    Kernel->>App: Ready

Dependency Resolution

Plugins declare dependencies:

{
  name: 'database-plugin',
  dependencies: ['logger', 'config'],
  optionalDependencies: ['metrics'],
  provides: ['database'],
  consumes: ['logger', 'config'],
}

The plugin registry:

  1. Builds a dependency graph from all registered plugins.
  2. Performs topological sort to determine registration order.
  3. Detects circular dependencies and throws an error.
  4. Validates that all required dependencies are present.
  5. Optional dependencies that are absent do not cause an error.

Capability Tokens

Capability tokens are strings that identify a capability, not a concrete type:

// Standard tokens defined in @setu-ts/common
const CAPABILITIES = {
  LOGGER: 'logger',
  DATABASE: 'database',
  CACHE: 'cache',
  AUTH: 'authentication',
  // ...
};

// Plugin A provides a capability
ctx.services.register(CAPABILITIES.DATABASE, new DatabaseService());

// Plugin B consumes the capability
const db = ctx.services.get<IDatabaseService>(CAPABILITIES.DATABASE);

Why tokens instead of class references?

  1. Decoupling — Plugin B does not import Plugin A’s code.
  2. Replaceability — Any plugin can provide the same token.
  3. Testing — Tests register a mock with the same token.
  4. Tree-shaking — No import dependencies between plugins.
  5. Multi-provider — Multiple plugins can provide the same token (e.g., multiple database connections).

Plugin Ordering

Plugins are ordered by:

  1. Dependency order — A plugin’s dependencies must be registered first.
  2. Priority — Within the same dependency level, lower priority numbers register first.
  3. Registration order — Within the same priority, registration order is preserved.

Plugin Replacement

Any plugin can be replaced by registering a new plugin with the same name and provides tokens:

// Replace the default database plugin
app.register({
  name: 'database', // Same name
  version: '1.0.0',
  provides: ['database'],
  register(ctx) {
    ctx.services.register('database', new MyCustomDatabase(), { override: true });
  },
});

Optional Plugins

Plugins can declare optional dependencies:

{
  name: 'audit-plugin',
  dependencies: ['logger'],
  optionalDependencies: ['database', 'messaging'],
  register(ctx) {
    const logger = ctx.services.get('logger');

    // Check if optional dependencies are available
    if (ctx.services.has('database')) {
      const db = ctx.services.get('database');
      // Use database for audit storage
    }

    if (ctx.services.has('messaging')) {
      const broker = ctx.services.get('messaging');
      // Publish audit events to message broker
    }
  },
}

Plugin Communication

Plugins never communicate directly. They communicate through:

MechanismUse Case
Service RegistryResolve a capability provided by another plugin
MiddlewareObserve or modify requests/responses
Event BusPublish/subscribe to domain events
Message BrokerPublish/subscribe to integration events
Lifecycle HooksReact to application lifecycle events

Lifecycle Hooks

Plugins register lifecycle hooks via ctx.lifecycle:

HookWhenUse Case
onRegisterPlugin registrationInitialize plugin state
onInitAll plugins registeredValidate cross-plugin state
onBootstrapServer about to startFinal preparation
onRequestEvery requestPer-request setup
onResponseEvery responsePer-response cleanup
onErrorUnhandled errorError logging, alerting
onStoppingstop() begins, still servingDeregister from a service registry
onShutdownApplication shutting downClose connections, flush buffers
onCloseApplication closedFinal cleanup

onStopping is the only hook that runs while the application is still answering requests normally: it fires at the very start of stop(), before the application begins refusing new requests with a 503 and before the socket closes. That ordering is what makes it the right home for telling the outside world to stop routing here — deregistering from service discovery, for instance. Deregistering in onShutdown instead means the socket is already closed by the time the registry hears about it, so callers keep being routed at a dead port for up to one health-check interval on every rolling deploy. Hooks run LIFO and are awaited, so a slow hook delays shutdown and a rejecting one surfaces from stop() — but only after the rest of the shutdown has run, so a failing hook cannot leave an application that keeps serving and can never be stopped; with no hook registered the phase is skipped entirely and stop() behaves exactly as it did before.

Extension Points

Every plugin must expose extension points so other plugins and application code can extend its behavior:

Extension PointAPIExample
Custom servicectx.services.register(token, service, { override: true })Replace the default logger
Custom middlewarectx.middleware.add(fn, { priority })Add a request timing middleware
Custom routectx.router.get(path, handler)Add a health check route
Custom health checkctx.health.register(name, indicator)Add a custom health indicator
Custom metricctx.metrics.register(name, config)Add a custom Prometheus metric
Custom CLI commandctx.cli.register(name, handler)Add a custom CLI command
Custom OpenAPIctx.openapi.addSchema(name, schema)Add a schema to the OpenAPI spec
Custom decoratorctx.decorators.register(name, handler)Add a custom decorator
Custom lifecyclectx.lifecycle.onX(fn)React to lifecycle events
Custom env validationctx.environment.validate(spec)Validate environment variables

6. Service Registry

The Service Registry is the primary service resolution mechanism in the framework. It maps capability tokens to service instances.

Registration

import { CAPABILITIES } from '@setu-ts/common';

// Register a service under a standard capability token
ctx.services.register(CAPABILITIES.DATABASE, new DatabaseService());

// Register with options (RegisterOptions has only `override` and `multi`)
ctx.services.register(CAPABILITIES.DATABASE, new DatabaseService(), {
  override: false, // Throw if already registered (default)
  multi: false, // Single provider (default)
});

// Lazy construction: the factory runs only on the first get()
ctx.services.registerFactory(CAPABILITIES.DATABASE, () => new DatabaseService());

Capability Lookup

// Get a service (throws if not found)
const db = ctx.services.get<IDatabaseService>(CAPABILITIES.DATABASE);

// Check if a capability is available
if (ctx.services.has(CAPABILITIES.CACHE)) {
  const cache = ctx.services.get<ICacheStore>(CAPABILITIES.CACHE);
}

// Get all services for a multi-provider token
const allNotifiers = ctx.services.getAll<INotifier>(CAPABILITIES.NOTIFICATION);

Multi-Provider Support

Some capabilities can have multiple providers:

// Plugin A
ctx.services.register(CAPABILITIES.NOTIFICATION, new EmailNotifier(), { multi: true });

// Plugin B
ctx.services.register(CAPABILITIES.NOTIFICATION, new SmsNotifier(), { multi: true });

// Consumer
const notifiers = ctx.services.getAll<INotifier>(CAPABILITIES.NOTIFICATION);
// Returns [EmailNotifier, SmsNotifier]

Override Rules

// First registration
ctx.services.register(CAPABILITIES.LOGGER, new PinoLogger());

// Override (explicit)
ctx.services.register(CAPABILITIES.LOGGER, new ConsoleLogger(), { override: true });

// Without override flag, throws an error
ctx.services.register(CAPABILITIES.LOGGER, new ConsoleLogger());
// Error: Capability 'logger' is already registered. Use { override: true } to replace.

Lazy Services

// Factory is called only on first get()
ctx.services.registerFactory(CAPABILITIES.DATABASE, () => {
  const config = ctx.services.get<IConfig>(CAPABILITIES.CONFIG);
  return new DatabaseService({ url: config.get('DATABASE_URL') });
});

// Factory is not called yet
// ...

// First get() triggers factory
const db = ctx.services.get<IDatabaseService>(CAPABILITIES.DATABASE); // Factory called here

// Subsequent get() returns cached instance
const db2 = ctx.services.get<IDatabaseService>(CAPABILITIES.DATABASE); // Same instance

Service Scopes

The Service Registry supports two scopes:

ScopeBehavior
ApplicationService is created once and shared across all requests. Default.
RequestService is created per request via ctx.services.register() in request middleware.

Request-scoped services are registered on the request context, not the global registry:

app.middleware.add(async (ctx, next) => {
  // Register a request-scoped service
  ctx.services.register('request-logger', logger.child({ requestId: ctx.id }));
  await next();
});

Registry Sealing

The application registry is sealed immediately after runBootstrap(). Its register, registerFactory, and unregister methods throw after that point; request contexts retain an unsealed child registry for request-scoped services. An override: true and a successful unregister during startup are reported through the logger capability.

Thread Safety Considerations

The Service Registry is not thread-safe by design. JavaScript is single-threaded (event loop), so concurrent access is not an issue. However:

  • Registration is enforced during bootstrap: the application registry seals after runBootstrap(), so post-bootstrap mutations throw.
  • Request-scoped registration is safe because each request has its own context.
  • Lazy factories are safe because JavaScript is single-threaded — the factory is called at most once.

7. Runtime Layer

Why Runtime Abstraction Exists

Different JavaScript runtimes have different APIs for the same operations:

OperationNode.jsDenoBun
UUIDcrypto.randomUUID()crypto.randomUUID()crypto.randomUUID()
Random bytescrypto.randomBytes()crypto.getRandomValues()crypto.randomBytes()
High-res timeprocess.hrtime()performance.now()performance.now()
Environmentprocess.envDeno.envBun.env
File systemfs moduleDeno.readFile()Bun.file()
HTTP serverhttp.createServer()Deno.serve()Bun.serve()

Without abstraction, every plugin would need runtime-specific code paths. The Runtime Layer eliminates this by providing a single interface.

IRuntimeServices

All runtime-specific operations are abstracted behind IRuntimeServices:

interface IRuntimeServices {
  platform(): RuntimePlatform;
  version(): string;
  hostname(): string;

  uuid(): string;
  randomBytes(length: number): Uint8Array;
  subtle: SubtleCrypto;

  now(): number;
  hrtime(): number;
  setTimeout(fn: () => void, ms: number): TimerHandle;
  clearTimeout(handle: TimerHandle): void;
  setInterval(fn: () => void, ms: number): TimerHandle;
  clearInterval(handle: TimerHandle): void;

  env: Record<string, string | undefined>;
  exit(code?: number): never;

  fs?: IFileSystem;
  workers?: IWorkerHost;
  dns?: IDnsResolver;
}

The optional members are capabilities a runtime may not have. Cloudflare Workers omits all three — dns in particular because its network access is fetch, which resolves names internally and exposes no lookup surface. An adapter omits an absent key entirely rather than assigning undefined to it, which exactOptionalPropertyTypes requires and which lets a consumer distinguish “not supported here” with a plain in check.

Runtime Adapters

graph LR
    subgraph RuntimePlugin
        Detector[Runtime Detector]
        Services[IRuntimeServices]
        HttpAdapter[HTTP Adapter]
    end

    subgraph Adapters
        Node[NodeRuntimeServices]
        Deno[DenoRuntimeServices]
        Bun[BunRuntimeServices]
        CF[CloudflareRuntimeServices]
    end

    Detector --> Node
    Detector --> Deno
    Detector --> Bun
    Detector --> CF

    Node --> Services
    Deno --> Services
    Bun --> Services
    CF --> Services

    Services --> HttpAdapter

Runtime Is Mandatory

A runtime provider is the one plugin every application must register. createApplication() fails fast at startup if no registered plugin provides the runtime capability, and the kernel always registers the runtime-providing plugin first, regardless of declared priority. This is why ctx.runtime on IPluginContext is non-optional — every other plugin can rely on it being present during its own register() call.

Auto-Detection

The RuntimePlugin also provides CAPABILITIES.LOCAL_DIAGNOSTICS_LISTENER (M98b): a runtime-owned factory that binds exactly one additional IPv4-loopback listener for the local diagnostics connector. The application’s own IHttpAdapter is stateful and cannot serve a second handler, so this factory owns a private server through an injectable Deno serve or Node HTTP seam — and exposes no adapter, host, or handle to its consumer, which supplies only a normalized-request protocol handler. Bun, Workers and unknown platforms refuse every listen before any bind, and the plugin registers a close hook so shutdown and failed-startup paths release the port.

The RuntimePlugin auto-detects the runtime at startup:

function detectRuntime(): RuntimePlatform {
  if (typeof Deno !== 'undefined') return 'deno';
  if (typeof Bun !== 'undefined') return 'bun';
  if (typeof caches !== 'undefined' && navigator.userAgent?.includes('cloudflare')) {
    return 'cloudflare-workers';
  }
  return 'node';
}

HTTP Server Abstraction

The Runtime Plugin also provides HTTP server adapters implementing the IHttpAdapter interface:

interface IHttpAdapter {
  setHandler(handler: (request: IRequest) => Promise<IResponse>): void;
  fetch(request: Request): Promise<Response>;
  listen(port: number, hostname?: string): Promise<ServerHandle>;
  close(handle: ServerHandle): Promise<void>;
  /** Optional: gRPC/Connect RPC interceptor. Consulted after WebSocket upgrade short-circuit and before body mapping. */
  setRpcHandler?(handler: RpcFetchHandler): void;
  /** Optional: WebSocket upgrade router. Consulted before body mapping. */
  setUpgradeRouter?(router: WebSocketUpgradeRouter): void;
}

The contract is web-fetch-centric: setHandler installs the framework handler, fetch is the universal entry point callable without listen (Cloudflare Workers), listen binds a real TCP socket, and close tears it down.

Each runtime has its own implementation:

  • NodeHttpAdapter — Uses @hono/node-server serve().
  • DenoHttpAdapter — Uses Deno.serve().
  • BunHttpAdapter — Uses Bun.serve().
  • CloudflareWorkersHttpAdapter — Exports fetch only; listen throws.

Node, Deno and Bun supply the socket peer to the shared mapping as IRequest.ip (M113n), normalizing IPv4-mapped IPv6. Workers and plain fetch calls have no peer. Behind a proxy the peer is the proxy; IP security checks it against trustedProxies before walking the forwarded chain, then publishes the client IP and public origin. Session cookies, CSRF and the React Router bridge read that origin through the common accessor.

Why Only Runtime May Use Runtime-Specific APIs

This rule is enforced because:

  1. Portability — If a plugin uses process.env directly, it breaks on Deno and Bun.
  2. Testability — Runtime services can be mocked in tests.
  3. Consistency — All plugins use the same API, regardless of runtime.
  4. Future-proofing — New runtimes only need a new adapter, not changes to every plugin.

Future Runtimes

Adding a new runtime requires:

  1. Implement IRuntimeServices for the new runtime.
  2. Implement IHttpAdapter for the new runtime.
  3. Add the runtime to the detector.
  4. No changes to any other package.

This is the power of the adapter pattern — the framework is open for extension but closed for modification.


8. Package Architecture

Toolchain and Distribution

The monorepo is developed with the Deno toolchain (Deno 2 workspaces; deno check/test/lint/fmt). Packages are published to JSR under the @setu-ts scope with no build step — TypeScript sources are published directly. Node and Bun consumers install through JSR’s npm compatibility layer, which transforms packages to standard ESM npm artifacts. Cross-runtime support is therefore a distribution property (JSR) plus a code property (runtime abstraction, §7) — CI verifies both by running the test suite under Deno and a compat suite under Node and Bun.

Package Overview

graph TB
    subgraph Foundation
        common[common]
        kernel[kernel]
        runtime[runtime]
    end

    subgraph Optional Core
        di[di-plugin]
        decorator[decorator-plugin]
    end

    subgraph Infrastructure Plugins
        database[database-plugin]
        cache[cache-plugin]
        messaging[messaging-plugin]
        queue[queue-plugin]
        storage[storage-plugin]
        service-discovery[service-discovery-plugin]
        cloudflare[cloudflare-plugin]
    end

    subgraph Security Plugins
        auth[auth-plugin]
        http-sec[http-security-plugin]
        secrets[secrets-plugin]
    end

    subgraph Application Plugins
        events[events-plugin]
        cqrs[cqrs-plugin]
        scheduler[scheduler-plugin]
    end

    subgraph Observability Plugins
        logger[logger-plugin]
        metrics[metrics-plugin]
        health[health-plugin]
        telemetry[telemetry-plugin]
        audit[audit-plugin]
    end

    subgraph Communication Plugins
        mail[mail-plugin]
        notification[notification-plugin]
    end

    subgraph Feature Plugins
        feature-flags[feature-flags-plugin]
        idempotency[idempotency-plugin]
        multi-tenancy[multi-tenancy-plugin]
        resilience[resilience-plugin]
        session[session-plugin]
    end

    subgraph Realtime Plugins
        sse[sse-plugin]
        websocket[websocket-plugin]
        realtime[realtime-backplane-plugin]
    end

    subgraph Platform Plugins
        graphql[graphql-plugin]
        grpc[grpc-plugin]
        react-router[react-router-plugin]
        static[static-plugin]
        view[view-plugin]
        localization[localization-plugin]
        worker-pool[worker-pool-plugin]
    end

    subgraph Developer Tools
        validation[validation-plugin]
        config[config-plugin]
        openapi[openapi-plugin]
        cli[cli]
        testing[testing]
        sdk[sdk]
        exceptions[exceptions]
    end

    subgraph Starters
        rest-starter[rest-starter]
        microservice-starter[microservice-starter]
        full-stack-starter[full-stack-starter]
    end

    common --> kernel
    common --> runtime
    kernel --> runtime
    common --> di
    kernel --> di
    common --> decorator
    kernel --> decorator

    common --> logger
    kernel --> logger
    common --> config
    kernel --> config
    common --> validation
    kernel --> validation
    common --> database
    kernel --> database
    common --> cache
    kernel --> cache
    common --> events
    kernel --> events
    common --> cqrs
    kernel --> cqrs
    common --> messaging
    kernel --> messaging
    common --> queue
    kernel --> queue
    common --> auth
    kernel --> auth
    common --> http-sec
    kernel --> http-sec
    common --> scheduler
    kernel --> scheduler
    common --> metrics
    kernel --> metrics
    common --> health
    kernel --> health
    common --> openapi
    kernel --> openapi
    common --> telemetry
    kernel --> telemetry
    common --> secrets
    kernel --> secrets
    common --> audit
    kernel --> audit
    common --> resilience
    kernel --> resilience
    common --> storage
    kernel --> storage
    common --> mail
    kernel --> mail
    common --> notification
    kernel --> notification
    common --> feature-flags
    kernel --> feature-flags
    common --> multi-tenancy
    kernel --> multi-tenancy
    common --> idempotency
    common --> service-discovery
    common --> cloudflare
    common --> session
    kernel --> session
    common --> graphql
    kernel --> graphql
    common --> grpc
    kernel --> grpc
    common --> sse
    kernel --> sse
    common --> websocket
    kernel --> websocket
    common --> realtime
    kernel --> realtime
    common --> react-router
    kernel --> react-router
    common --> static
    kernel --> static
    common --> view
    kernel --> view
    common --> localization
    kernel --> localization
    common --> worker-pool
    kernel --> worker-pool
    common --> rest-starter
    kernel --> rest-starter
    common --> microservice-starter
    kernel --> microservice-starter
    common --> full-stack-starter
    kernel --> full-stack-starter
    ```

### Package Details

#### @setu-ts/common

| Aspect               | Detail                                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Purpose**          | Shared types, interfaces, and capability tokens                                                                                      |
| **Responsibilities** | Define all public interfaces; define capability token constants; provide shared utility types                                        |
| **Dependencies**     | None                                                                                                                                 |
| **Public API**       | All interfaces (`IPlugin`, `IPluginContext`, `ILogger`, `IConfig`, `IDatabaseService`, etc.); `CAPABILITIES` constant; utility types |
| **Extension Points** | N/A (types only)                                                                                                                     |
| **Rules**            | Zero dependencies; no runtime behavior beyond constants and pure type utilities                                                      |

#### @setu-ts/kernel

| Aspect               | Detail                                                                                                                                    |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Plugin orchestration and request processing                                                                                               |
| **Responsibilities** | Plugin registry; service registry; middleware pipeline; router; application lifecycle; request context                                    |
| **Dependencies**     | `common` only                                                                                                                             |
| **Public API**       | `createApplication()`; `Application`; `ServiceRegistry`; `RouterApi`; `MiddlewareApi` (consumes `IPlugin`/`IPluginContext` from `common`) |
| **Extension Points** | Custom plugins; custom middleware; custom routes; service override                                                                        |
| **Rules**            | No runtime-specific APIs; no decorator support; no DI; zero features beyond orchestration                                                 |

#### @setu-ts/runtime

| Aspect               | Detail                                                                                               |
| -------------------- | ---------------------------------------------------------------------------------------------------- |
| **Purpose**          | Runtime abstraction layer                                                                            |
| **Responsibilities** | Provide `IRuntimeServices` for Node, Deno, Bun; provide HTTP server adapters; runtime auto-detection |
| **Dependencies**     | `common`, `kernel`                                                                                   |
| **Public API**       | `RuntimePlugin()`; `IRuntimeServices`; `IHttpAdapter`; `detectRuntime()`                             |
| **Extension Points** | Custom runtime adapter (for new runtimes); custom HTTP adapter                                       |
| **Rules**            | The only package allowed to use runtime-specific APIs                                                |

#### @setu-ts/di-plugin

| Aspect               | Detail                                                                                                      |
| -------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Optional dependency injection container                                                                     |
| **Responsibilities** | Provide DI container with singleton, scoped, transient lifecycles; constructor injection; factory providers |
| **Dependencies**     | `common`, `kernel`                                                                                          |
| **Public API**       | `DiPlugin()`; `IContainer`; `ContainerBuilder`                                                              |
| **Extension Points** | Custom provider types; custom scopes                                                                        |
| **Rules**            | Optional; no other plugin depends on it; services resolve from ServiceRegistry, not container               |

#### @setu-ts/decorator-plugin

| Aspect               | Detail                                                                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Optional decorator and metadata system                                                                                                |
| **Responsibilities** | Store decorator metadata in plain objects; read metadata and register routes, services, middleware, and explicit non-HTTP ingress classes through public capabilities |
| **Dependencies**     | `common`, `kernel`                                                                                                                    |
| **Public API**       | `DecoratorPlugin()`; `@Controller`, `@Get`, `@Post`, etc.; `@Injectable`, `@Inject`; `@Params(...)` with the `Body`/`Query`/`Param` sources; `createDecorator()` |
| **Extension Points** | Custom decorators via `createDecorator()`; custom parameter sources via `Custom()`                                                                |
| **Rules**            | Optional; no reflection required; metadata stored in plain objects; decorators are syntactic sugar over programmatic API              |

#### @setu-ts/logger-plugin

| Aspect               | Detail                                                                                                      |
| -------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Structured logging                                                                                          |
| **Responsibilities** | Provide `ILogger` implementations (Pino, Console, Noop); request logging middleware; slow request detection; redaction of log metadata |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                               |
| **Public API**       | `LoggerPlugin()`; `ILogger`                                                                                 |
| **Extension Points** | Custom logger implementation (override `logger` token); custom log formatters                               |
| **Rules**            | Pino is optional (injected or lazy-loaded via a bare specifier); ConsoleLogger works on all runtimes        |

#### @setu-ts/config-plugin

| Aspect               | Detail                                                                                             |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| **Purpose**          | Configuration management                                                                           |
| **Responsibilities** | Load environment variables; parse `.env` files; validate config with Zod; provide type-safe access |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                      |
| **Public API**       | `ConfigPlugin()`; `IConfig`                                                                        |
| **Extension Points** | Custom config sources (override `config` token); custom env file parsers                           |
| **Rules**            | Environment access via `runtime.env`, not `process.env`                                            |

#### @setu-ts/validation-plugin

| Aspect               | Detail                                                                                                               |
| -------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Request validation with Zod                                                                                          |
| **Responsibilities** | Validate body, query, params, headers, cookies; sanitize input; format validation errors (RFC 9457, default, custom) |
| **Dependencies**     | `common`, `kernel`                                                                                                   |
| **Public API**       | `ValidationPlugin()`; `IValidationService`; `validateBody()`, `validateQuery()`, `validateParams()`                  |
| **Extension Points** | Custom error formatters; custom sanitization rules                                                                   |
| **Rules**            | Zod is the source of truth for validation; schemas are shared with OpenAPI plugin                                    |

#### @setu-ts/exceptions

| Aspect               | Detail                                                                                                                  |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Exception types and global error handling                                                                               |
| **Responsibilities** | `HttpError` type; factory functions (`badRequest()`, `notFound()`, etc.); error handler middleware; RFC 9457 formatting |
| **Dependencies**     | `common` only                                                                                                           |
| **Public API**       | `HttpError`; exception factory functions; `errorHandler()`                                                              |
| **Extension Points** | Custom error formatters                                                                                                 |
| **Rules**            | Plain package, not a plugin; composition (factories) over inheritance; registered by the application, not the kernel    |

#### @setu-ts/database-plugin

| Aspect               | Detail                                                                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Database access with repository pattern                                                                                               |
| **Responsibilities** | Provide `IDatabaseService`; repository pattern including portable expression filters and `findOne`; unit of work (transactions); backend adapters (Memory, Prisma, Drizzle, MongoDB, DynamoDB, Cosmos DB, Cloud Bigtable)                   |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                         |
| **Public API**       | `DatabasePlugin()`; `IDatabaseService`; `IRepository`; `IUnitOfWork`; Drizzle-specific `createDrizzleDatabase()`, `getDrizzleDatabase()`, and `getDrizzleTransaction()` |
| **Extension Points** | Custom ORM adapter; custom repository; custom transaction strategy                                                                    |
| **Rules**            | Applications inject Prisma clients and opaque configured Drizzle identities with explicit Promise-aware transaction bridges; only Drizzle operators are lazy-loaded from bare specifiers. Portable repository operations stay backend-neutral, while released raw `query()` and distinct typed outer/transaction Drizzle accessors are explicit backend-specific escape hatches. Memory remains the zero-dependency default. Cloud Bigtable is the wide-column arm: it has no secondary index, so its row key — one lexicographically-sorted string composed from logical fields by the per-entity mapping — is the only ordering, and every non-key predicate is evaluated by the same portable evaluator the memory reference uses. |

#### @setu-ts/cache-plugin

| Aspect               | Detail                                                                                                                        |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Caching abstraction with transparent response-caching middleware                                                              |
| **Responsibilities** | Provide `ICacheStore`; Memory, Redis, Noop stores; `CacheService`; `cacheMiddleware`; TTL management                          |
| **Dependencies**     | `common`, `kernel`                                                                                                            |
| **Public API**       | `CachePlugin()`, `CacheService`, `MemoryStore`, `RedisStore`, `NoopStore`, `cacheMiddleware`, types in `src/interfaces/index` |
| **Extension Points** | Custom cache store (override `cache` token); custom key generators; named multi-cache instances                               |
| **Rules**            | Redis client is optional (injected or lazy-loaded via a bare specifier); Memory store default; `ICacheStore` has 5 methods    |

#### @setu-ts/events-plugin

| Aspect               | Detail                                                                                                                     |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | In-memory event bus for domain events                                                                                      |
| **Responsibilities** | Publish/subscribe domain events; event handler registration; error handling                                                |
| **Dependencies**     | `common`, `kernel`                                                                                                         |
| **Public API**       | `EventsPlugin()`; `IEventBus`; `DomainEvent`; `IntegrationEvent`; `defineDomainEvent`; `IDomainEvents`; `createDomainEvents`; `IEventHandler`; `subscribeHandler` |
| **Extension Points** | Custom event bus (override `events` token); custom event handlers                                                          |
| **Rules**            | In-memory only; `IDomainEvents` records aggregate-local facts without publishing; for distributed events, use messaging plugin |

#### @setu-ts/cqrs-plugin

| Aspect               | Detail                                                                                                                                                                                                                                                                   |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Purpose**          | Command Query Responsibility Segregation                                                                                                                                                                                                                                 |
| **Responsibilities** | Command bus; query bus; pipeline behaviors; handler registration                                                                                                                                                                                                         |
| **Dependencies**     | `common`, `kernel`                                                                                                                                                                                                                                                       |
| **Public API**       | `CqrsPlugin()`; `ICommandBus`; `IQueryBus`; `ICqrsFacade`; `ICommandHandler`; `IQueryHandler`; `IPipelineBehavior`; `CqrsRequest`; `CqrsCommand`; `CqrsQuery`; `CommandBus`; `QueryBus`; `HandlerNotFoundError` (contracts owned by `common`, re-exported by the plugin) |
| **Extension Points** | Custom pipeline behaviors; custom bus implementations                                                                                                                                                                                                                    |
| **Rules**            | Optional; consumes the `events` capability via token for event sourcing (optional)                                                                                                                                                                                       |

#### @setu-ts/messaging-plugin

| Aspect               | Detail                                                                                                                                                                                                                                              |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Message broker abstraction for cross-service integration events                                                                                                                                                                                     |
| **Responsibilities** | Provide `IMessageBroker`; in-memory, Redis Streams, RabbitMQ, NATS, Kafka, GCP Pub/Sub, Azure Service Bus, and custom-injected broker adapters; serializer interface (`ISerializer`); `EventsMessagingBridge`; request-reply via `RequestReplyCore` |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                                                                                                                                       |
| **Public API**       | `MessagingPlugin()`; `EventsMessagingBridge()`; `InMemoryBroker`; `RedisStreamsBroker`; `RabbitMqBroker`; `NatsBroker`; `KafkaBroker`; `GcpPubSubBroker`; `ServiceBusBroker`; `JsonSerializer`; `IMessageBroker` (re-exported from `common`)        |
| **Extension Points** | Custom broker adapter; custom serializers; custom SDK client injection; custom transport adapters                                                                                                                                                   |
| **Rules**            | Cloud SDKs are optional (injected or lazy-loaded via a bare specifier); in-memory broker default for testing; decoupled from events plugin; named instances via `name` option; cloud brokers require provider credentials at runtime                |

#### @setu-ts/queue-plugin

| Aspect               | Detail                                                                                                                                                                                                                                                                                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Purpose**          | Background job queue                                                                                                                                                                                                                                                                                                                                               |
| **Responsibilities** | Add/process jobs; retry strategies; recurring jobs; concurrency control; Memory/Redis/RabbitMQ/SQS adapters; AWS SNS fan-out publisher; cloud DLQ promotion                                                                                                                                                                                                        |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                                                                                                                                                                                                                                                      |
| **Public API**       | `QueuePlugin()`; `MemoryQueue`; `RedisQueue`; `RabbitMqQueue`; `SqsQueue`; `SnsPublisher`; `IQueue` (re-exported from `common`)                                                                                                                                                                                                                                    |
| **Extension Points** | Custom queue adapter via internal `QueueAdapter` seam; custom retry strategies; custom SQS transport injection                                                                                                                                                                                                                                                     |
| **Rules**            | Redis client is optional (injected or lazy-loaded via a bare specifier); Memory queue for testing; RabbitMQ adapter (polling via basicGet, TTL+DLX for delays); SQS adapter via AWS SDK `@aws-sdk/client-sqs` (injected or lazy-loaded); SNS publisher via AWS SDK `@aws-sdk/client-sns` (injected or lazy-loaded); DLQ forwards original body then deletes source |

**Architecture Notes:** The `QueuePlugin` factory registers a `QueueService` implementing the
`IQueue` contract under the `queue` capability token (or `queue.<name>` for named instances). The
service owns the backend-agnostic machinery (worker poll loop, retry with exponential backoff,
cron-driven recurring scheduling) and delegates storage to an internal `QueueAdapter` transport
seam. `MemoryQueue` and `RedisQueue` (ioredis-based delayed queue using Redis sorted sets) implement
this seam in M15. `RabbitMqQueue` (implemented in M15b) uses polling via `basicGet` over a ready
queue and per-message TTL + dead-letter-exchange for delayed enqueue/requeue. `SqsQueue` (M54) uses
AWS SQS with visibility timeouts, envelope-based retry tracking, and DLQ promotion when
`maxAttempts` is reached — the DLQ adapter forwards the original message body to the DLQ queue URL
then deletes the source message, with separate diagnostics for send vs delete failures. The
`SnsPublisher` export is a separate AWS SNS fan-out helper (not a `QueueAdapter`): it publishes to a
single topic and reuses the same inject-or-lazy `@aws-sdk/client-sns` SDK seam as `SqsQueue`.

#### @setu-ts/idempotency-plugin

| Aspect               | Detail                                                                                                                                                                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Purpose**          | One idempotency core that answers a repeat of completed work from its record (or skips it), reachable from HTTP, ingress and plain code. It deduplicates; it cannot make a handler's external side effects atomic with the record |
| **Responsibilities** | The `claim`/`complete`/`release` state machine; memory, Redis (three Lua scripts) and Cloudflare Durable Object stores; the HTTP `idempotent()` middleware; the `idempotentIngress()` behaviour; the `idempotency` health indicator                     |
| **Dependencies**     | `common` only — ioredis is injected or lazy-loaded, and the Durable Object store lives in `cloudflare-plugin` because no plugin imports a plugin                                                                                                   |
| **Public API**       | `IdempotencyPlugin()`; `idempotent()`; `derivedIdempotencyKey()`; `idempotentIngress()`; `IdempotencyRefusedError`; `IdempotencyConfigurationError`; `IDEMPOTENCY_KEY_HEADER`; `IDEMPOTENT_REPLAYED_HEADER`; and the types `IdempotencyPluginOptions`, `IdempotencyStoreConfig`, `IRedisIdempotencyClient`, `IdempotencyRefusalReason`. The stores and `IIdempotencyService`/`IIdempotencyStore` are internal or owned by `common`, not barrel exports |
| **Extension Points** | A custom `IIdempotencyStore` through the `store` option; the `scope`, `key` and `fingerprint` callbacks; `@Idempotent()` in `decorator-plugin` resolves the same capability token                                                                  |
| **Rules**            | `idempotent()` and `@Idempotent()` are appended AFTER guards and validation, so a refused request consumes no key; the Redis tier owns its client rather than widening `ICacheStore`; tier A is per process, so Workers HTTP uses the Durable Object store |

#### @setu-ts/auth-plugin

| Aspect               | Detail                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Authentication and authorization (JWT, API key, local credentials; RBAC; route guards)                                                                                                                                                                                                                                                                                                                                                                              |
| **Responsibilities** | JWT service (HS256/RS256) for self-issued tokens; an `issuers` strategy verifying outside-provider access tokens against their published, rotating key sets (RS256/PS256/ES256/ES384/EdDSA) — a strategy, not a service, so `IJwtService` stays self-issued only; API-key + local credential strategies; RBAC with role hierarchy; permission checks; short-circuiting guards; PBKDF2 password hashing                                                                                                                                                                                                                                                                                                     |
| **Dependencies**     | `common`, `kernel`                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| **Public API**       | `AuthPlugin()`; `IAuthService`; `IJwtService`; `IAuthorizationService`; `authMiddleware()`; `PasswordHasher`; guards `requireAuth()`, `requireRole()`, `requirePermission()`, `requireAnyRole()`, `requireAllPermissions()`, `publicRoute()`                                                                                                                                                                                                                        |
| **Extension Points** | Custom `IAuthStrategy` strategies; RBAC role/permission models; app-supplied `apiKey.validate` / `local.verify`                                                                                                                                                                                                                                                                                                                                                     |
| **Rules**            | Zero npm deps — JWT (HS256/RS256) and PBKDF2-SHA256 via Web Crypto (`runtime.subtle`/`runtime.randomBytes`); no JWT library. `jwt` and `authentication` always register; `authorization` registers only when `rbac` is supplied. `ctx.request.user` is the writable principal field; guards short-circuit (no `next()`) on 401/403. Refresh tokens (`RefreshTokenService`) + rate limiting (`rateLimitMiddleware`) shipped in M16b as standalone additions — no new tokens |

#### @setu-ts/http-security-plugin

| Aspect               | Detail                                                                                                                                                      |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | HTTP transport security                                                                                                                                     |
| **Responsibilities** | CORS; security headers; CSRF; request size limiting; IP security                                                                                            |
| **Dependencies**     | `common`, `kernel`                                                                                                                                          |
| **Public API**       | `HttpSecurityPlugin()`; `corsMiddleware()`; `securityHeadersMiddleware()`; `csrfMiddleware()`; `requestSizeMiddleware()`; `ipSecurityMiddleware()`          |
| **Extension Points** | Custom security headers; custom CORS policies; custom CSRF trusted origins                                                                                  |
| **Rules**            | Secure defaults: security headers on by default; CORS/CSRF/request-size/IP-security are opt-in via their option blocks, each secure-by-default when enabled |

#### @setu-ts/scheduler-plugin

| Aspect               | Detail                                                                                                         |
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Job scheduling                                                                                                 |
| **Responsibilities** | Cron jobs; delayed jobs; recurring jobs; retry with backoff; distributed locking                               |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                  |
| **Public API**       | `SchedulerPlugin()`; `IScheduler`                                                                              |
| **Extension Points** | Custom distributed lock implementation; custom cron parser                                                     |
| **Rules**            | Distributed locking for multi-instance deployments; Redis for lock storage (optional, injected or lazy-loaded) |

#### @setu-ts/metrics-plugin

| Aspect               | Detail                                                                                          |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| **Purpose**          | Prometheus metrics                                                                              |
| **Responsibilities** | Counter, gauge, histogram, summary; metrics registry; Prometheus rendering; built-in collectors |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                   |
| **Public API**       | `MetricsPlugin()`; `IMetricsService`                                                            |
| **Extension Points** | Custom metric collectors; custom renderers                                                      |
| **Rules**            | Prometheus format only; OpenMetrics support in future                                           |

#### @setu-ts/health-plugin

| Aspect               | Detail                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------ |
| **Purpose**          | Health check endpoints                                                                     |
| **Responsibilities** | `/health`, `/live`, `/ready` endpoints; health indicator registration; built-in indicators |
| **Dependencies**     | `common`, `kernel`                                                                         |
| **Public API**       | `HealthPlugin()`; `IHealthService`; `IHealthIndicator`                                     |
| **Extension Points** | Custom health indicators                                                                   |
| **Rules**            | Health indicators depend on capability tokens, not concrete packages                       |

#### @setu-ts/openapi-plugin

| Aspect               | Detail                                                                          |
| -------------------- | ------------------------------------------------------------------------------- |
| **Purpose**          | OpenAPI documentation generation                                                |
| **Responsibilities** | Generate OpenAPI spec from route schemas; Zod to OpenAPI conversion; Swagger UI |
| **Dependencies**     | `common`, `kernel`                                                              |
| **Public API**       | `OpenApiPlugin()`; `IOpenApiService`                                            |
| **Extension Points** | Custom schema transformers; custom UI                                           |
| **Rules**            | Zod is the source of truth; no duplicated schemas; spec deduplication           |

#### @setu-ts/telemetry-plugin

| Aspect               | Detail                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | OpenTelemetry distributed tracing                                                                                                                                                                                                                                                                                                                                                                 |
| **Responsibilities** | Tracer provider; span management; context propagation; request-span middleware; safe request URL attributes                                                                                                                                                                                                                                                                                      |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                                                                                                                                                                                                                                                                                     |
| **Public API**       | `TelemetryPlugin()`; `telemetryMiddleware`; `TELEMETRY_SPAN_KEY`; `NoopTelemetryService`; `InstrumentationsConfig`/`InstrumentationConfig`/`InstrumentationKind`/`SpanProcessorKind`                                                                                                                                                                                                              |
| **Extension Points** | Custom exporters; injectable `tracerProviderFactory`; injectable per-instrumentation instances (`InstrumentationConfig.instrumentation`)                                                                                                                                                                                                                                                          |
| **Rules**            | OpenTelemetry SDK is optional (injected or lazy-loaded via a bare specifier); noop default when no exporter; middleware priority 30; `TracerProviderFactory` injection seam for tests; auto-instrumentation (M24b) is runtime-gated (Node-only kinds `http`/`fetch`/`ioredis`/`amqplib`/`kafkajs`) and degrades to a documented no-op elsewhere; `spanProcessor: 'simple'` (default) or `'batch'` |

#### @setu-ts/secrets-plugin

| Aspect               | Detail                                                                                                |
| -------------------- | ----------------------------------------------------------------------------------------------------- |
| **Purpose**          | Secret management                                                                                     |
| **Responsibilities** | Retrieve secrets from KMS, Vault, env; secret rotation; caching                                       |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                         |
| **Public API**       | `SecretsPlugin()`; `ISecretManager`                                                                   |
| **Extension Points** | Custom secret provider                                                                                |
| **Rules**            | Cloud SDKs are optional (injected or lazy-loaded via bare specifiers); env provider for development |

#### @setu-ts/audit-plugin

| Aspect               | Detail                                                                   |
| -------------------- | ------------------------------------------------------------------------ |
| **Purpose**          | Audit trail logging                                                      |
| **Responsibilities** | Log audit events; redact entry bodies; store in database, file, or log; audit trail retrieval |
| **Dependencies**     | `common`, `kernel` (consumes `logger` capability via token)              |
| **Public API**       | `AuditPlugin()`; `IAuditLogger`                                          |
| **Extension Points** | Custom audit storage                                                     |
| **Rules**            | Audit logs are immutable; storage is pluggable                           |

#### @setu-ts/resilience-plugin

| Aspect               | Detail                                                        |
| -------------------- | ------------------------------------------------------------- |
| **Purpose**          | Resilience patterns                                           |
| **Responsibilities** | Circuit breaker; retry with backoff; timeout; bulkhead        |
| **Dependencies**     | `common`, `kernel`                                            |
| **Public API**       | `ResiliencePlugin()`; `IResilienceService`                    |
| **Extension Points** | Custom resilience patterns; custom circuit breaker strategies |
| **Rules**            | Patterns are composable; no external dependencies             |

#### @setu-ts/storage-plugin

| Aspect               | Detail                                                                                                          |
| -------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | File storage abstraction                                                                                        |
| **Responsibilities** | S3, GCS, local, memory providers; upload middleware; signed URLs                                                |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                   |
| **Public API**       | `StoragePlugin()`; `IStorage`                                                                                   |
| **Extension Points** | Custom storage provider                                                                                         |
| **Rules**            | Cloud SDKs are optional (injected or lazy-loaded via bare specifiers); local and memory providers for testing |

#### @setu-ts/mail-plugin

| Aspect               | Detail                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| **Purpose**          | Email sending                                                                                     |
| **Responsibilities** | SMTP, SES, SendGrid providers; template engine (`{{ variable }}` strings and view components)     |
| **Dependencies**     | `common`, `kernel`; optionally the `view` capability (component templates, M102)                  |
| **Public API**       | `MailPlugin()`; `IMailer`                                                                         |
| **Extension Points** | Custom mail provider; component templates through `CAPABILITIES.VIEW`                            |
| **Rules**            | Email SDKs are optional (injected or lazy-loaded via bare specifiers); log provider for testing |

#### @setu-ts/notification-plugin

| Aspect               | Detail                                                                         |
| -------------------- | ------------------------------------------------------------------------------ |
| **Purpose**          | Multi-channel notifications                                                    |
| **Responsibilities** | Email, SMS, push, Slack channels; multi-channel dispatch                       |
| **Dependencies**     | `common`, `kernel`                                                             |
| **Public API**       | `NotificationPlugin()`; `INotifier`                                            |
| **Extension Points** | Custom notification channels                                                   |
| **Rules**            | Channel providers are optional (injected or lazy-loaded via bare specifiers) |

#### @setu-ts/feature-flags-plugin

| Aspect               | Detail                                                                                    |
| -------------------- | ----------------------------------------------------------------------------------------- |
| **Purpose**          | Feature flag management                                                                   |
| **Responsibilities** | Flag evaluation; percentage rollout; user targeting; multiple providers                   |
| **Dependencies**     | `common`, `kernel`                                                                        |
| **Public API**       | `FeatureFlagsPlugin()`; `IFeatureFlags`                                                   |
| **Extension Points** | Custom flag provider                                                                      |
| **Rules**            | Config provider for simple cases; DatabaseProvider or LaunchDarklyProvider for production |

#### @setu-ts/realtime-backplane-plugin

| Aspect               | Detail                                                                                   |
| -------------------- | ---------------------------------------------------------------------------------------- |
| **Purpose**          | Cross-replica fan-out for WebSocket rooms and SSE channels                               |
| **Responsibilities** | Publish/subscribe transport; origin stamping and echo suppression; frame encoding        |
| **Dependencies**     | `common`                                                                                 |
| **Public API**       | `RealtimeBackplanePlugin()`; `IRealtimeBackplane`                                        |
| **Extension Points** | Custom transport via the `'custom'` arm                                                  |
| **Rules**            | Consumed OPTIONALLY — absent, rooms and channels stay in-process. Never imports a plugin |

`cloudflare-plugin` is a **second provider** of `CAPABILITIES.REALTIME_BACKPLANE` (M52d, over a
Durable Object). An application registers exactly one of the two: the kernel rejects two providers
of a single capability token at startup. It is likewise a second provider of
`CAPABILITIES.MESSAGING` (M59, over Cloudflare Queues plus a Durable Object reply inbox) — an
application registers `MessagingPlugin` **or** the Cloudflare `messaging` arm, never both.

#### @setu-ts/multi-tenancy-plugin

| Aspect               | Detail                                                                            |
| -------------------- | --------------------------------------------------------------------------------- |
| **Purpose**          | Multi-tenancy support                                                             |
| **Responsibilities** | Tenant resolution; tenant context; database isolation strategies; cache isolation |
| **Dependencies**     | `common`, `kernel`                                                                |
| **Public API**       | `MultiTenancyPlugin()`; `IMultiTenancyService`                                    |
| **Extension Points** | Custom tenant resolver; custom database strategy                                  |
| **Rules**            | Tenant resolution via middleware; tenant context via request context              |

#### @setu-ts/testing

| Aspect               | Detail                                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Purpose**          | Testing utilities                                                                                                                                      |
| **Responsibilities** | Test app factory; mock plugin; request injection; mock context                                                                                         |
| **Dependencies**     | `common`, `kernel`                                                                                                                                     |
| **Public API**       | `createTestApp()`; `createMockPlugin()`; `inject()`; `collectStream()`; `createTestContext()`; `MockResponse`; `MockServiceRegistry`; `FixtureManager` |
| **Extension Points** | N/A (testing utility)                                                                                                                                  |
| **Rules**            | No real external dependencies; in-memory adapters only                                                                                                 |

#### @setu-ts/cli

| Aspect               | Detail                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | CLI tool with generators                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| **Responsibilities** | Project scaffolding; monorepo workspaces; code generation; plugin-aware generators                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **Dependencies**     | `common`, `runtime` (NOT `kernel` — the CLI emits source text and loads the user's app through `IApplication`, never constructing one itself)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| **Public API**       | The `setu` command (`new`, `new --workspace`, `generate`, `generate app`, `commands`); `runCli`, `deriveNames`, `detectPlugins`, `PROGRAM_NAME`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| **Extension Points** | Custom schematics from `.setu-ts/schematics/`; plugin commands via `ICliApi`, read by loading the project's `setu.config.ts`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **Rules**            | Generation reads the target project's manifest and its generated-artifact directories, and never boots it; schematics are pure functions returning files, so `--dry-run` writes nothing. A generate refuses to overwrite any existing file, except the CLI-owned seam barrels a schematic declares `managed` (one `index.ts` per generated family). It also refuses a name that would collide with an existing artifact's HTTP path or injection token. Generation has two coherent styles, selected by the template and persisted in the generated manifest: the default is FUNCTIONAL — no `DecoratorPlugin`, no `DiPlugin`, `ctx`-first handlers and plain-function services — while `--template class-based` installs both plugins together and switches every style-sensitive schematic to decorated classes. There is no independent `--di` axis; a project's installed package set IS the selector, so a later `generate` cannot silently revert to the other style. Every host — including the no-template one — wires the seams whose plugins it registers, so `setu generate route` is a first-class decorator-free path. Plugin-command discovery is the one path that DOES boot the app — via `setu.config.ts`, with no port and guaranteed teardown. A **workspace** is a Deno workspace by default, or an npm one under `--runtime node|bun` (Cloudflare Workers is refused — each Worker is its own deploy unit), whose root declares members by GLOB (`"workspace": ["./apps/*"]`, or `"workspaces"` on the npm arm), so `setu generate app` creates a member directory and rewrites no manifest; framework pins live in each member's own manifest, because plugin detection reads one directory and never walks up. Cross-member service discovery is a CLI-owned `src/discovery/services.ts` per member, rendered from `setu.workspace.json` — the LOCAL development topology only; Compose and Kubernetes objects per member belong to M39. A template that a runtime serves through a DIFFERENT plugin declares a `RuntimeSwap` rather than refusing the pairing: `microservice` on `cloudflare-workers` swaps `MessagingPlugin`/`QueuePlugin` for `CloudflarePlugin`, which serves both capabilities from Cloudflare Queues, and contributes the module-level `queue` export, the Durable Object class the platform requires the application to own, and the wrangler stanzas it needs. A standalone `setu new` may select its OWN message broker and job queue with `--broker`/`--queue`, derived from the SAME transport registry the workspace `--transport` flag reads; each flag is REFUSED, never silently ignored, wherever it would be a no-op (Cloudflare Workers, starter-composed templates, templates registering no matching wiring), and the selected arm's Compose service ships with the scaffold so it can boot. Prompting is an OPTIONAL `ask` seam on `CliDependencies` that fails closed in three layers — absent by default, TTY-gated in the executable entry point, refused by Deno on a non-terminal — so every gate and script is non-interactive by construction |

#### @setu-ts/sdk

| Aspect               | Detail                                                                                                                                                                                                                                                                                                                                      |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Portable, zero-npm-dependency client SDK for consuming Setu-TS APIs from browsers and servers                                                                                                                                                                                                                                               |
| **Responsibilities** | Injectable HTTP client (`IHttpClient`); bearer/API-key auth interceptors; per-origin retry with backoff, circuit breaker, sliding-window rate limiting; pure OpenAPI 3.1 → TypeScript codegen                                                                                                                                               |
| **Dependencies**     | `common` only (type-level imports of `RetryPolicy`, `CircuitBreakerPolicy`, `BackoffStrategy`; no kernel, no runtime, no plugins, no npm dependencies)                                                                                                                                                                                      |
| **Public API**       | Interfaces + factory functions + error classes (`IHttpClient`, `createClient()`, interceptor types, `IClientTiming`, `createDefaultClientTiming()`, `ClientRateLimitPolicy`, re-exported policy types, auth factories, errors, `generateOpenApiClient`, `SdkOpenApi*` types, `OpenApiCodegenOptions`) — NOT the concrete `HttpClient` class |
| **Extension Points** | Custom request/response interceptors (`ClientRequestInterceptor`, `ClientResponseInterceptor`); injected `fetch` and `IClientTiming` seams                                                                                                                                                                                                  |
| **Rules**            | Not a plugin; does not register capabilities or resolve tokens; runtime-independent (web `URL`, `Headers`, `AbortSignal`, `performance.now()` only); zero npm dependencies                                                                                                                                                                  |

#### @setu-ts/cloudflare-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Cloudflare Workers platform bindings: KV, R2, D1, Queues, Messaging, Cron, Cache API, Durable Objects                                 |
| **Responsibilities** | Bind platform-specific services to the service registry; split env bindings; throw on missing bindings                                  |
| **Dependencies**     | `common`, `kernel`, `storage-plugin`, `cache-plugin`, `database-plugin`                                                                 |
| **Public API**       | `CloudflarePlugin()`; `ICloudflareBindings`                                                                                             |
| **Extension Points** | N/A — platform-specific, not replaceable                                                                                                |
| **Rules**            | Workers-only; zero npm dependencies; bindings injected at app construction, never imported from `cloudflare:workers`                    |

#### @setu-ts/graphql-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | GraphQL server with schema-first and code-first arms, over HTTP, WebSocket, and SSE                                                   |
| **Responsibilities** | Schema parsing; query execution; subscription management; GraphiQL interface; depth limiting; APQ                                       |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `GraphqlPlugin()`; `IGraphqlService`                                                                                                    |
| **Extension Points** | Custom schema; custom resolvers; custom scalar resolvers; custom document cache                                                         |
| **Rules**            | `graphql` is inject-or-lazy; subscriptions require `CAPABILITIES.WEBSOCKET` or `CAPABILITIES.SSE` OPTIONALLY                   |

#### @setu-ts/grpc-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Co-serve gRPC, Connect, and gRPC-Web on the same port as ordinary Hono routes                                                           |
| **Responsibilities** | Connect-ES core + Protobuf-ES; reflection and health over embedded descriptors; RPC dispatch through the kernel pipeline              |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `GrpcPlugin()`; `IGrpcService`                                                                                                          |
| **Extension Points** | Custom service definitions via embedded `FileDescriptorSet` constants                                                                   |
| **Rules**            | Connect-ES + Protobuf-ES from bare specifiers; detection is prefix-only; RPC dispatch runs in the kernel terminal handler after the middleware pipeline |

#### @setu-ts/react-router-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | React Router 8 framework-mode SSR embedded as a plugin                                                                                |
| **Responsibilities** | File-based routing; SSR via `loadRequestHandler`; static asset serving over `runtime.fs` with symlink-safe containment                  |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `ReactRouterPlugin()`; `SsrService`                                                                                                     |
| **Extension Points** | Custom `loadRequestHandler` seam; custom `loadContext`                                                                                  |
| **Rules**            | Vite is an app-level build tool, never imported by the plugin; `IFileSystem.realPath` is the symlink containment seam                   |

#### @setu-ts/sse-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Server-Sent Events with named channels, heartbeat, and Last-Event-ID replay                                                             |
| **Responsibilities** | Frame encoding; channel management; heartbeat interval; client disconnect handling                                                      |
| **Dependencies**     | `common`, `kernel`                                                                                                                      |
| **Public API**       | `SsePlugin()`; `ISseService`                                                                                                            |
| **Extension Points** | Custom frame encoder; custom channel store                                                                                              |
| **Rules**            | One-way broadcast only; uses `IResponse.stream()` for the SSE body                                                                      |

#### @setu-ts/websocket-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Full-duplex WebSocket on all four runtimes, with rooms, heartbeat, and admission control                                                |
| **Responsibilities** | RFC 6455 handshake; room broadcast with `except`; idle sweeper; max connections / max message bytes                                     |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `WebSocketPlugin()`; `IWebSocketService`                                                                                                |
| **Extension Points** | Custom room implementation; custom heartbeat; custom admission control                                                                  |
| **Rules**            | `setUpgradeRouter?` on `IHttpAdapter` is the interception seam; real-socket e2e on Deno is required                                    |

#### @setu-ts/worker-pool-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | CPU-bound work on real worker threads, off the event loop                                                                               |
| **Responsibilities** | Task handlers addressed by module specifier; structured clone I/O; per-specifier lazy task pools                                        |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `WorkerPoolPlugin()`; `IWorkerPool`                                                                                                     |
| **Extension Points** | Custom `IWorkerHost` implementation; custom task handler modules                                                                        |
| **Rules**            | `IRuntimeServices.workers?` is the seam; omitted on Workers (throws `WorkerPoolUnavailableError`)                                      |

#### @setu-ts/session-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Cookie sessions with pluggable stores and synchronizer-token form CSRF                                                                  |
| **Responsibilities** | AES-256-GCM / HKDF-SHA256 cookie sealing; rolling / idle timeout; store strategy (memory / cache / custom)                             |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `SessionPlugin()`; `ISessionService`; `csrfTokenField()`                                                                               |
| **Extension Points** | Custom `ISessionStore`; custom cookie signing (`mode: 'sign'`)                                                                          |
| **Rules**            | Zero npm dependencies; `runtime.subtle` for crypto; commit-on-response is sound via `snapshot()` live `Headers`                        |

#### @setu-ts/static-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Static file serving as a capability rather than a side effect of SSR                                                                    |
| **Responsibilities** | Conditional requests; single-range `206`; `.br`/`.gz` sidecar negotiation; SPA fallback                                                |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `StaticPlugin()`; `IStaticFiles`                                                                                                        |
| **Extension Points** | Custom `IFileSystem` implementation; custom content-type map                                                                            |
| **Rules**            | `IRuntimeServices.fs?` is the seam; strong ETag when `mtime` present, weak when size-only                                              |

#### @setu-ts/view-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Server-rendered HTML as a capability — views named by reference, never by path                                                          |
| **Responsibilities** | Register an `IViewEngine` under `CAPABILITIES.VIEW`; `'hono-jsx'` / `'hono-html'` / `'custom'` arms; `view` health indicator; the `Suspense` refusal |
| **Dependencies**     | `common`, `hono` (both default arms add no new third-party package to the resolution set)                                          |
| **Public API**       | `ViewPlugin()`; `renderView()`; `raw`; `ViewRenderError`; `UnresolvedSuspenseError`; `ViewPluginOptions`                                 |
| **Extension Points** | The `'custom'` engine arm; `@Render(Component)` in `decorator-plugin` resolves the same token                                            |
| **Rules**            | No filesystem lookup, so Workers-portable by construction; output is a buffered primitive string, never a stream; rendering is stateless, so no `onClose` |

#### @setu-ts/localization-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Message catalogues per locale, request locale resolution, and a formatter shared with the browser                                       |
| **Responsibilities** | Register an `ILocalizer` under `CAPABILITIES.LOCALIZATION`; validate catalogues at `register()`; resolve `IRequest.locale` in middleware at 45; write `Vary` and `Content-Language`; the opt-in catalogue route; `localization` health indicator |
| **Dependencies**     | `common` only; no npm package — plurals, numbers and dates come from the platform's `Intl`                                              |
| **Public API**       | `LocalizationPlugin()`; `localizerFor()`; `localeMiddleware()`; three error classes; the `/format` subpath (`format`, `negotiateLocale`, `parseAcceptLanguage`) |
| **Extension Points** | `IMessageSource` for catalogues kept outside the code; `tenantLocale` for a per-tenant default; other plugins resolve `ILocalizer` from `common` |
| **Rules**            | Client text only ever selects a CONFIGURED locale and never reaches a header; the `/format` subpath has no runtime dependency outside itself (enforced by a `deno info` gate); the formatter escapes nothing; `cache-plugin` keys on the resolved locale |

#### @setu-ts/service-discovery-plugin

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Service discovery over static config, Consul, Kubernetes, or DNS-SRV, with balancing and outlier ejection                               |
| **Responsibilities** | Read-through cache with per-service in-flight coalescing; stale-on-failure; watch-based invalidation                                   |
| **Dependencies**     | `common`, `kernel`, `runtime`                                                                                                           |
| **Public API**       | `ServiceDiscoveryPlugin()`; `IServiceDiscovery`                                                                                         |
| **Extension Points** | Custom `DiscoveryProvider` arm; custom `IDnsResolver`                                                                                   |
| **Rules**            | Zero npm dependencies; `IRuntimeServices.dns?` is the optional DNS seam; `ILifecycleApi.onStopping` for deregistration                  |

#### @setu-ts/rest-starter

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Opinionated REST plugin composition library: `createRestApp` with pre-wired plugin set                                                  |
| **Responsibilities** | Compose `RuntimePlugin`, `LoggerPlugin`, `ExceptionsPlugin`, `ValidationPlugin`, `HealthPlugin`, `MetricsPlugin`, `OpenApiPlugin`     |
| **Dependencies**     | `common`, `kernel`, `runtime`, `logger-plugin`, `exceptions`, `validation-plugin`, `health-plugin`, `metrics-plugin`, `openapi-plugin` |
| **Public API**       | `createRestApp()`; `RestStarterOptions`                                                                                                 |
| **Extension Points** | Per-plugin option arms; optional `realtime` / `di` gates                                                                                 |
| **Rules**            | Emits inline wiring, never imports a starter; `rest` = 9 plugins + `errorHandler()` middleware                                          |

#### @setu-ts/microservice-starter

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Opinionated microservice composition: REST set plus messaging, queues, resilience, telemetry                                            |
| **Responsibilities** | Compose `rest` starter plus `MessagingPlugin`, `QueuePlugin`, `ResiliencePlugin`, `TelemetryPlugin`                                    |
| **Dependencies**     | `rest-starter`, `messaging-plugin`, `queue-plugin`, `resilience-plugin`, `telemetry-plugin`                                             |
| **Public API**       | `createMicroserviceApp()`; `MicroserviceStarterOptions`                                                                                 |
| **Extension Points** | `serviceDiscovery` arm; `realtime` / `di` gates                                                                                         |
| **Rules**            | Refused on `cloudflare-workers` because brokers need raw sockets                                                                        |

#### @setu-ts/full-stack-starter

| Aspect               | Detail                                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose**          | Opinionated full-stack composition: microservice set plus caching, CQRS, storage, and React Router SSR                                  |
| **Responsibilities** | Compose `microservice` starter plus `CachePlugin`, `CqrsPlugin`, `StoragePlugin`, `ReactRouterPlugin`                                  |
| **Dependencies**     | `microservice-starter`, `cache-plugin`, `cqrs-plugin`, `storage-plugin`, `react-router-plugin`                                          |
| **Public API**       | `createFullStackApp()`; `createFullStackAppFromConfig()`; `FullStackStarterOptions`                                                     |
| **Extension Points** | `reactRouter`, `session`, `database` arms; config-driven composition via `ConfigPlugin`                                                 |
| **Rules**            | Vite is an app-level build tool; `tsconfig.base.json` enables `exactOptionalPropertyTypes`                                 |

---

9. Dependency Rules

Allowed Dependency Direction

graph TB
    common[common]
    kernel[kernel]
    runtime[runtime]
    plugins[All Plugins]

    common --> kernel
    common --> runtime
    common --> plugins
    kernel --> runtime
    kernel --> plugins

    style common fill:#e1f5fe
    style kernel fill:#e8f5e9
    style runtime fill:#fff3e0
    style plugins fill:#fce4ec

Rules:

  1. common depends on nothing.
  2. kernel depends on common only.
  3. runtime depends on common and kernel.
  4. All plugins depend on common and optionally kernel.
  5. No plugin depends on another plugin at the package level.
  6. Plugins communicate via capability tokens at runtime.

Why Circular Dependencies Are Forbidden

Circular dependencies cause:

  1. Unpredictable initialization — If A depends on B and B depends on A, which initializes first?
  2. Bundle size — Bundlers cannot tree-shake circular dependencies.
  3. Testing difficulty — Cannot test one package without loading the other.
  4. Maintenance — Changes to one package force changes to the other.

How Capability Tokens Prevent Circular Dependencies

graph LR
    subgraph Without Tokens
        A1[Plugin A] --> B1[Plugin B]
        B1 --> A1
    end

    subgraph With Tokens
        A2[Plugin A] -->|register token| R[Service Registry]
        B2[Plugin B] -->|get by token| R
    end

With capability tokens:

  • Plugin A registers a service with token 'database'.
  • Plugin B resolves the service by token 'database'.
  • Plugin B never imports Plugin A’s code.
  • No circular dependency.

Build-Time vs Runtime Dependencies

TypeDirectionExample
Build-timePackage imports types from another packageimport type { IDatabaseService } from '@setu-ts/common'
RuntimePlugin resolves service by tokenctx.services.get('database')

Build-time dependencies are for type definitions only. Runtime dependencies are resolved through the service registry. This separation ensures packages can be type-checked independently and bundled without pulling in unnecessary code.


10. Middleware Pipeline

Pipeline Architecture

graph LR
    Req[Request] --> M1[Middleware 1]
    M1 --> M2[Middleware 2]
    M2 --> M3[Middleware 3]
    M3 --> Handler[Route Handler]
    Handler --> M3
    M3 --> M2
    M2 --> M1
    M1 --> Resp[Response]

The middleware pipeline is a chain of functions that execute in order. Each middleware receives the request context and a next() function. Calling next() passes control to the next middleware. Not calling next() short-circuits the pipeline.

The pipeline runs for ALL inbound traffic, not only ordinary HTTP routes. WebSocket upgrades and gRPC/Connect requests go through the same chain before anything protocol-specific happens, so auth, metrics and the shutdown drain apply to them exactly as they do to a GET /users. The handshake or RPC dispatch happens only in the kernel’s terminal handler, after the pipeline has run without short-circuiting — a guard that answers 401 therefore refuses the upgrade, and a draining application answers 503 on every protocol.

Protocol dispatch runs before route matching, not after. A WebSocket upgrade is a protocol switch rather than an HTTP route, and a path inside the gRPC basePath belongs to gRPC — so an application catch-all (the one ReactRouterPlugin mounts for SSR, a SPA fallback, a hand-written 404 route) cannot shadow either.

One caveat, because it is easy to assume otherwise: on an accepted upgrade the adapter answers with the runtime’s own 101, so response headers a middleware wrote on ctx.response — security headers, Set-Cookie — are not carried onto the handshake response. The pipeline still runs, which is what lets a guard refuse; it just has no response to decorate once the socket is taken over. A refused upgrade is an ordinary HTTP response and carries everything.

This was not always so. Until M70a the HTTP adapter consulted setUpgradeRouter and setRpcHandler before mapping the request, so no middleware applied to either: an unauthenticated WebSocket wrote through a guarded endpoint and an unauthenticated gRPC client read and wrote through one. The adapter now stores the router and acts only on an upgrade intent the kernel records after the pipeline; setRpcHandler is deprecated and consulted by nothing.

Ordering

Middleware is ordered by priority (lower numbers execute first). The pipeline is an onion: pre-next() code runs in ascending priority order, then the route handler (terminal), then post-next() code unwinds in descending order. A LOW priority number is outermost (first inbound, last outbound).

PriorityMiddlewarePurpose
0ErrorHandlerOutermost error handler
20MetricsMiddlewareRecord metrics
30TelemetryMiddlewarePropagate trace context
40TenantMiddlewareResolve request tenant
45LocaleMiddlewareResolve request locale
50LoggingMiddlewareLog incoming request
100RequestIdMiddlewareGenerate request ID
150CorrelationIdMiddlewarePropagate correlation ID
200CorsMiddlewareHandle CORS
250SecurityHeadersMiddlewareAdd security headers
260SessionMiddlewareLoad/commit the session
270CsrfMiddlewareOrigin/Referer check
300AuthMiddlewareAuthenticate request
325CsrfFormMiddlewareSynchronizer-token check
350AuthorizationMiddlewareCheck permissions
400ValidationMiddlewareValidate request

These priorities are conventional bands. Metrics (20), telemetry (30), tenant resolution (40), locale resolution (45), HTTP security (120–270), session (260), authentication (300), and form CSRF (325) are self-registered by their own plugins. The application (or a starter) adds the error handler and any application middleware. AuthPluginOptions.middleware can move authentication, exclude selected paths, or disable its global registration when the application attaches authMiddleware() per route.

The session sits below authentication so an auth strategy can read it — SessionStrategy (configured by AuthPluginOptions.session) is that reader. It opens the cookie through ISessionService.fromHeaders, a headers-only read, so it needs only the session capability (SessionPlugin) to be registered; it does not itself require the session middleware to have run first. The session’s commit phase (which runs after next() returns) therefore wraps everything inner. The two CSRF middlewares are different mechanisms rather than one feature configured twice: the stateless Origin/Referer check at 270 is cheap and runs first, so a request failing it never reaches token comparison at 325. The form check sits after authentication (300) because its bearer exemption reads the credential source the authentication middleware records — only a request authenticated by a header-credential strategy is exempt. Anything reading the session must be registered above 260, or the session accessor throws.

Registration

// Global middleware
app.middleware.add(loggingMiddleware(), { priority: 50 });

// Route-level middleware
app.router.get('/users', {
  middleware: [requireAuth(), validationMiddleware()],
  handler: async (ctx) => {
    /* ... */
  },
});

Execution

sequenceDiagram
    participant Pipeline
    participant MW1 as Middleware 1
    participant MW2 as Middleware 2
    participant Handler

    Pipeline->>MW1: ctx, next()
    MW1->>MW1: Pre-processing
    MW1->>MW2: next()
    MW2->>MW2: Pre-processing
    MW2->>Handler: next()
    Handler-->>MW2: Response
    MW2->>MW2: Post-processing
    MW2-->>MW1: Response
    MW1->>MW1: Post-processing
    MW1-->>Pipeline: Final response

Short-Circuiting

A middleware can short-circuit by sending a response without calling next():

const authMiddleware = () => async (ctx, next) => {
  const token = ctx.request.headers.get('authorization');
  if (!token) {
    return ctx.response.status(401).json({ error: 'Unauthorized' });
    // next() is not called — pipeline stops
  }
  await next();
};

Non-HTTP ingress behaviour chain

Queue processors, scheduler jobs, messaging subscriptions, and WebSocket frames use a second, transport-neutral behaviour chain. Each work item is presented to an IIngressBehavior as an IngressContext; calling next() advances to the next declared behaviour and a behaviour that does not call it short-circuits the handler.

This ingress chain is separate from both the HTTP middleware pipeline and CQRS’s IPipelineBehavior chain. They do not share middleware or behaviour instances: HTTP middleware wraps requests and upgrades, CQRS behaviours wrap commands and queries, and ingress behaviours wrap the four non-HTTP handler kinds above.

Error Propagation

If a middleware or handler throws an error:

  1. The error propagates up the middleware chain.
  2. Each middleware’s post-processing is skipped.
  3. The error reaches the error handler middleware.
  4. The error handler formats the response and sends it.
graph LR
    Handler -->|throws Error| MW2
    MW2 -->|skips post-processing| MW1
    MW1 -->|skips post-processing| ErrorHandler
    ErrorHandler -->|formats response| Client

Context Creation

Each request gets a fresh RequestContext:

interface RequestContext {
  request: IRequest;
  response: IResponse;
  services: ServiceRegistry;
  state: Map<string, unknown>;
  params: Record<string, string>;
  query: Record<string, string>;
  id: string;
  startTime: number;
}

The state map allows middleware to pass data to downstream middleware and handlers:

// Validation middleware
ctx.state.set(validatedStateKey('body'), result.data);

// Handler
const body = ctx.state.get(validatedStateKey('body'));

Request-Scoped Data

Request-scoped data is stored on the context, not on global state. This ensures:

  • No data leaks between requests.
  • No race conditions.
  • Easy testing (each test gets a fresh context).

11. Dependency Injection

Why DI Is Optional

DI is a pattern for managing service dependencies. In Setu-TS, the Service Registry is the primary service resolution mechanism. DI is an optional layer that provides:

  • Constructor injection.
  • Lifecycle management (singleton, scoped, transient).
  • Circular dependency detection at the DI level.

Applications that prefer manual wiring or factory functions can skip the DI plugin entirely.

Three Approaches to Service Management

graph TB
    subgraph Approach 1: Service Registry Only
        SR1[ctx.services.get token] --> S1[Service Instance]
    end

    subgraph Approach 2: Manual Wiring
        MW[Constructor Injection] --> S2[Service Instance]
    end

    subgraph Approach 3: DI Plugin
        DI[DI Container] --> SR2[Service Registry]
        SR2 --> S3[Service Instance]
    end
ApproachWhen to Use
Service Registry onlySimple applications; functional style; no DI needed
Manual wiringWhen you want explicit control; pass dependencies via constructors
DI PluginWhen you want automatic resolution; when you have complex dependency graphs

Service Registry (Default)

// Register
ctx.services.register('userService', new UserService(db, logger));

// Resolve
const userService = ctx.services.get<UserService>('userService');

Manual Wiring

class UserService {
  constructor(
    private db: IDatabaseService,
    private logger: ILogger,
  ) {}
}

// In a plugin
register(ctx) {
  const db = ctx.services.get('database');
  const logger = ctx.services.get('logger');
  ctx.services.register('userService', new UserService(db, logger));
}

DI Plugin

// Register DI plugin
app.register(DiPlugin({ defaultScope: 'singleton' }));

// In a plugin
register(ctx) {
  const container = ctx.services.get<IContainer>('di-container');

  container.register('UserService', {
    useClass: UserService,
    inject: ['database', 'logger'],
  });

  // Resolve with constructor injection
  const userService = container.resolve<UserService>('UserService');
}

When to Use Each

ScenarioRecommended Approach
Simple app with few servicesService Registry
Functional programming styleService Registry
OOP with explicit dependenciesManual Wiring
Complex dependency graphsDI Plugin
NestJS migrationDI Plugin + Decorator Plugin
TestingService Registry (mock by overriding token)

12. Decorator System

Why Decorators Are Optional

Decorators are a language feature not every environment or bundler supports, and some developers prefer a functional style over OOP decorators. Setu-TS uses TC39 standard decorators, so no compiler option is required — but a runtime whose toolchain does not transform them (Node, for instance, where V8 has not shipped them) still needs one that does.

Setu-TS makes decorators optional by:

  1. Providing a complete programmatic API for every feature.
  2. Implementing decorators as a thin layer over the programmatic API.
  3. Storing decorator metadata in plain objects, not via reflection.

How Decorators Work

graph LR
    D[Decorator] -->|stores metadata| M[Metadata Store]
    M -->|read by| DP[DecoratorPlugin]
    DP -->|registers| K[Kernel]
    K -->|creates| R[Routes, Services, Middleware]
  1. Decorator execution — When a class is decorated, the decorator function stores metadata in a plain object (MetadataStore).

  2. Metadata storage — Metadata is stored in a Map keyed by class reference. No reflection is used.

  3. DecoratorPlugin registration — When the DecoratorPlugin is registered, it scans the MetadataStore for decorated classes.

  4. Kernel registration — The DecoratorPlugin calls the kernel’s programmatic API to register routes, services, and middleware from the metadata.

Metadata Storage

// MetadataStore is a plain object, not a WeakMap
interface MetadataStore {
  controllers: Map<Function, ControllerMetadata>;
  services: Map<Function, ServiceMetadata>;
  routes: Map<Function, RouteMetadata[]>;
}

Why not WeakMap? WeakMaps require the key to be an object reference. In some bundlers and runtimes, class references can be lost during tree-shaking. A plain Map with explicit registration is more predictable.

Programmatic API Equivalents

Every decorator has a programmatic equivalent:

DecoratorProgrammatic Equivalent
@Controller('/users')app.router.group('/users', ...)
@Get('/')app.router.get('/', handler)
@Params(Body())ctx.request.body
@Params(Query('name'))ctx.query.name
@Params(Param('id'))ctx.params.id
@UseGuards(requireAuth())middleware: [requireAuth()]
@Injectable()ctx.services.register(token, new Service())

The three parameter rows show the raw equivalent. Body(), Query() and Param() prefer the value validation middleware wrote for the request (validatedStateKey(...)) and fall back to the raw request only when there is none — so on a route carrying @ValidateBody(schema) they yield the parsed-and-transformed value, where ctx.request.body yields what the client sent. A schema with a coercion or a default therefore makes the two differ.

Reflection

Reflection is not required. The DecoratorPlugin reads metadata from the MetadataStore, not via Reflect.getMetadata(). This ensures:

  • No dependency on reflect-metadata polyfill.
  • Works in environments that do not support reflection.
  • Metadata is explicit and inspectable.

Discovery

The DecoratorPlugin can auto-discover decorated classes:

app.register(
  DecoratorPlugin({
    autoDiscover: true,
    controllersPath: './src/controllers',
  }),
);

Or classes can be registered explicitly:

app.register(
  DecoratorPlugin({
    controllers: [UserController, OrderController],
  }),
);

13. Error Handling

Exception Flow

graph TB
    Handler[Route Handler] -->|throws Error| Pipeline
    Pipeline -->|catches| ErrorHandler[Error Handler Middleware]
    ErrorHandler -->|logs| Logger[Logger]
    ErrorHandler -->|formats| Formatter[Error Formatter]
    Formatter -->|RFC 9457| Response[Error Response]
    Response --> Client[Client]

Exception Types

The framework uses factory functions instead of class inheritance for exceptions:

// Factory function (composition)
function badRequest(message: string, details?: unknown): HttpError;
function unauthorized(message: string): HttpError;
function notFound(message: string): HttpError;

Why factory functions?

  1. No rigid inheritance hierarchy to maintain.
  2. Custom error types without modifying the base class.
  3. Easier serialization/deserialization.
  4. Runtime composition of error properties.

Problem Details (RFC 9457)

The standards-compliant error format follows RFC 9457, which obsoleted RFC 7807 in July 2023:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "User with id 123 not found",
  "instance": "/users/123"
}

type is about:blank because an HttpError carries no problem-type identity beyond its status code, and RFC 9457 §4.2 registers about:blank for exactly that case — a URI minted from the status would identify nothing status does not already convey. The one error that does define an extension member, validationError(), gets a concrete type URI (https://setu-ts.dev/errors/validation).

The 'rfc7807' format alias is retained, deprecated, and unchanged; it still emits the status-derived type. Removal is scheduled for v1.0.0.

Global Error Handler

The error handler middleware comes from the @setu-ts/exceptions package and is registered by the application (or by a starter bundle) — the kernel ships zero features, including error formatting:

import { errorHandler } from '@setu-ts/exceptions';

app.middleware.add(
  errorHandler({
    format: 'rfc9457',
    includeStackTrace: config.get('NODE_ENV') === 'development', // via ConfigPlugin, never process.env
    logErrors: true,
  }),
);

For an application-owned response to a caught error, respond can return or resolve a result built with the live context’s response writer (for example, ctx.response.status(error.statusCode).html(page)). It runs after normalization, masking, and served-status resolution; its returned result owns the response, while undefined falls through to the configured formatter. Logging retains the unmasked diagnostic and records the selected response’s status. This is an explicit application branch, not content negotiation, and it does not intercept the responder terminals below.

Error Responder Seam

The kernel ships zero error formatting, but it does produce error responses of its own (a malformed request URL → 400, an unmatched path → 404, an unhandled error → 500, a request during a shutdown drain → 503), and so does every short-circuiting middleware. Before M70f each of those sites wrote its own ad-hoc body, so an application that configured errorHandler({ format: 'rfc9457' }) still saw a mix of formats.

The fix is a request-scoped seam in @setu-ts/common — not exceptions, because no plugin may import exceptions (where every formatter lives), and the kernel and every middleware that answers an error all need the same contract. errorHandler builds an IErrorResponder from the formatter and content type it already resolved at factory time and publishes it in ctx.state before next(); any site inside the pipeline then calls respondWithError(ctx, { status, title, detail?, details? }), which delegates to the published responder and answers in the application’s configured format. The one first-party site deliberately outside the seam is validation-plugin, which owns its own errorFormat option and formats validation failures itself; an application sets the two to the same format, as the CLI templates and rest-starter do. The responder builds a real HttpError from the init (so buildProblemDetails sees a genuine statusCode and the validation errors extension, and maskInternalErrors never masks a deliberate 4xx), runs the resolved formatter over it, and writes status, content-type, and the serialized body — the same three-step tail errorHandler’s catch path performs. Three kernel sites run before the pipeline — the shutdown-drain 503, the malformed-URL / undecodable-path 400, and an onRequest lifecycle hook that throws (answered as the unhandled 500) — so they cannot see errorHandler’s ctx.state publication; the kernel seeds the same resolved responder into their state from a cache it reads off the pipeline’s errorHandler at startup, and the fallback below applies when no errorHandler is registered.

With no errorHandler registered, respondWithError falls back to the no-handler shape { error, detail? } — the framework’s pre-formatter shape, written directly by the seam and not the same as the default formatter’s { statusCode, message, details? } body — so a site can never answer in a shape the application did not ask for. The unhandled-error 500 additionally logs the error through CAPABILITIES.LOGGER when a logger is registered (X11-2); the body stays opaque. serializeError (also in common) is the pure serializer the logger plugin uses to normalize a raw Error out of log metadata before redaction (X2-5) and the kernel, exceptions, grpc-plugin, and notification-plugin use to serialize a cause without importing one another.

Plugin Error Handling

Plugins can register custom error handlers:

ctx.lifecycle.onError((error, ctx) => {
  const logger = ctx.services.get('logger');
  logger.error('Unhandled error', { error: error.message, stack: error.stack });
});

Logging and Observability

Telemetry spans nest through the telemetry host’s activation seam. In a real OTel configuration the plugin registers an async-local context manager and executes withSpan callbacks with their span active. This is what makes spans produced by downstream work, including broker publishing, children of the request span; without activation they are unrelated siblings.

Activation is conditional, not guaranteed: it requires real OTel mode, contextPropagation left enabled, and a context manager that registered successfully. Noop and fallback modes, an explicit contextPropagation: false, and a failed registration all leave spans flat — recorded, but unparented. The outcome is reported through the plugin’s logger rather than thrown, so a component that needs a specific parent passes parentContext explicitly instead of relying on ambient activation.

Errors are:

  1. Logged — Via the logger plugin (if registered).
  2. Traced — Via the telemetry plugin (if registered), as span exceptions.
  3. Metriced — Via the metrics plugin (if registered), as error counters.
  4. Audited — Via the audit plugin (if registered), for security-related errors.

14. Security Model

Security Layers

graph TB
    subgraph Transport Security
        CORS[CORS]
        Headers[Security Headers]
        CSRF[CSRF]
        Size[Request Size Limiting]
        IP[IP Security]
    end

    subgraph Authentication
        JWT[JWT]
        APIKey[API Key]
        Local[Local Strategy]
        Refresh[Refresh Token]
    end

    subgraph Authorization
        RBAC[RBAC]
        Perms[Permissions]
        Guards[Guards]
    end

    subgraph Secrets
        KMS[KMS/Vault]
        Env[Env Secrets]
        Rotation[Secret Rotation]
    end

    subgraph Rate Limiting
        RL[Rate Limiter]
        Storage[Redis/Memory Storage]
    end

Authentication

Authentication is handled by the AuthPlugin, which provides:

  • JWT — Sign and verify tokens.
  • API Key — Validate API keys via custom function.
  • Local — Username/password authentication.
  • Refresh Token — Issue new access tokens via the app-instantiated RefreshTokenService (M16b, shipped) — a service with rotation + revocation over a pluggable store, not an IAuthStrategy (a refresh token arrives in the request body, not as a passive header).

Authentication middleware extracts credentials and populates ctx.request.user.

Authorization

Authorization is handled via free MiddlewareFunction factories (imported from the plugin, not methods on IAuthService). The role/permission guards resolve IAuthorizationService from the 'authorization' token and short-circuit — they return a 401/403 response and do not call next(); authMiddleware always calls next() so an unauthenticated request still reaches the guard.

  • requireAuth() — Require any authenticated user (401).
  • requireRole(role) — Require a specific role, honoring the configured hierarchy (401/403).
  • requirePermission(permission) — Require a specific permission (401/403).
  • requireAnyRole(roles) — Require any of the specified roles.
  • requireAllPermissions(permissions) — Require all specified permissions.
  • publicRoute() — Explicitly allow unauthenticated access (named publicRoute, not public).

Policies (Milestone 110a)

Roles answer “does this principal hold this role, anywhere”; a policy answers “may this principal do this, to this target”. IAuthorizationService is synchronous and takes a principal plus a role or permission string, so a target cannot reach it — which is why policies are a second capability, IAuthorizationPolicyService under CAPABILITIES.AUTHORIZATION_POLICIES, beside RBAC rather than a widening of it. AuthPlugin always registers it.

Why one evaluator. The route guard (requirePolicy), the decorator (@RequirePolicy), and the imperative can/authorize all reach the same service, so the four entry points cannot disagree about an outcome; a parity test drives all four under one non-default configuration. @RequirePolicy and requirePolicy are two thin middlewares rather than one because decorator-plugin may not import auth-plugin (§2.2); both call the public contract, so an application’s replacement provider serves them too.

Why the rules are fixed. Only a literal true allows; a throw denies (and is logged without the target); an anonymous principal is refused 401 unless the ability opted in. Configurable fail-open-ish semantics are exactly the knobs an authorization layer should not have.

Why unknown names fail at startup. A functional guard is a value the application builds, so no register() ever sees it. AuthPlugin’s onBootstrap hook scans every registered route for the policy-guard brand and fails start() before the server listens, then seals the registry. @RequirePolicy is validated in DecoratorPlugin’s own register(). A route added after start() is not scanned and fails closed per request.

Where attribute rules go. Into policies. The framework ships no attribute language — ASP.NET Core, NestJS and Spring make the same choice. An external engine (OpenFGA, Casbin) is wrapped as a policy. Scoped grants (a role held in one tenant) are built on this layer — see Scoped Roles below. Non-HTTP ingress has no policy behaviour yet: IngressContext carries no principal, and a check that silently evaluated as anonymous would be worse than none; the imperative calls work there because the principal is an argument.

Scoped Roles (Milestone 110b)

The RBAC layer below holds roles globally: “Ann is an approver”. Scoped RBAC holds them in a scope — “Ann is an approver in the Acme tenant”, or in one organisation, team or region. It extends the global model rather than contradicting it: the role catalogue is unchanged and still the only source of role and permission names; what a grant adds is where it applies.

Why a built-in policy, not a capability. scopedRbac makes AuthPlugin define one reserved policy, scoped-rbac, on the M110a evaluator: each catalogue permission is an ability perm:<name>, each role role:<name>, and the target is the scope. So the guards (requireScopedRole/requireScopedPermission), the decorators (@ScopedRoles/@ScopedPermissions) and the imperative can reach the same evaluator and inherit its fixed rules, and the M110a startup scan refuses a typo in a scoped guard for free — a typo is a configuration error, not a 403.

One mechanism for hierarchy and delegation. inheritsFrom(scope) names the scopes whose grants also apply; a parent is one hop and a delegating tenant another. It is walked transitively, depth-first, with cycle refusal and depth/node bounds, so a malformed hierarchy denies rather than loops. Grants flow down only: a child request inherits a parent’s grants, never the reverse.

Why the request tenant bounds the scope. A scope taken from a route parameter is caller-controlled. A check whose own scope is a tenant other than the resolved request tenant denies, so a parameter cannot reach outside the caller’s tenant; a parent reached through inheritsFrom still applies.

Why one failing source denies. Grant sources are unioned. Answering from the sources that did reply would allow on a partial view — the one grant that says “no longer” may be the one that failed. Every source call has a deadline, every result is validated and bounded, and the deny is logged with the source name, the scope type and the error class — never an identifier.

Why custom roles bundle permissions. A tenant may define its own roles at runtime, but only from catalogue permissions, resolved against the defining scope only, and checked through permissions. The permission catalogue stays static, so the startup check stays total.

Why timing is a choice. Resolving per request makes a revocation immediate; a TTL cache or sign-in resolution trades revocation latency for fewer reads. Sign-in grants live under a private session key, never in principal claims, so a federated token carrying a same-named claim grants nothing.

RBAC

The RBAC system supports role hierarchy:

rbac: {
  roles: {
    admin: { permissions: ['*'], inherits: ['manager'] },
    manager: { permissions: ['users:read', 'users:write'], inherits: ['user'] },
    user: { permissions: ['profile:read', 'profile:write'] },
  },
}

Secrets

The SecretsPlugin provides secret management:

  • AWS KMS — Retrieve secrets from AWS KMS.
  • GCP Secret Manager — Retrieve secrets from GCP.
  • Azure Key Vault — Retrieve secrets from Azure.
  • HashiCorp Vault — Retrieve secrets from Vault.
  • Environment — Retrieve secrets from environment variables (development).

Plugin Responsibilities

PluginSecurity Responsibility
http-security-pluginCORS, security headers, CSRF, request size, IP security
auth-pluginAuthentication, authorization, RBAC, route guards
secrets-pluginSecret management and rotation
audit-pluginAudit trail for security events; policy redaction
validation-pluginInput validation and sanitization
logger-pluginRedaction of structured log metadata
telemetry-pluginQuery-safe span URL attributes; optional value redaction

Secure Defaults

All security plugins default to the most secure configuration:

  • CORS: no origins allowed by default.
  • JWT: strong algorithm (HS256 or RS256).
  • Rate limiting: enabled by default.
  • Security headers: enabled by default.
  • Input validation: enabled by default.
  • Secret redaction in logs: enabled by default.

Diagnostics Capture Boundary (Milestone 98a)

Kernel diagnostics are OFF unless createApplication({ diagnostics }) is passed explicitly, and an omitted option allocates no collector, ring, or timer. When enabled, the security boundary is STRUCTURAL rather than a filter over captured data: the kernel selects bounded primitives at its own registration and execution boundaries, so no dynamic request path, query string, header, body, cookie, token, session value, database value, or raw error text is ever collected in the first place. There is nothing to redact afterwards, and snapshot()/read() cannot resolve a lazy factory, invoke application code, or mutate state.

Registration names are a separate decision: plugin names, capability tokens, route patterns and middleware names appear only when the application listed that exact string in diagnostics.labels, which makes approving one a deliberate disclosure rather than a secret-detection guarantee. A plugin’s version is the one field NOT gated that way — it is emitted whenever it passes a bounded semver grammar. Two trust limits are explicit: the projection is not a sandbox against malicious same-process code, and it cannot isolate deliberately blocking synchronous application code. Network authentication and transport for an external devtool are a separate milestone (M98b); this boundary is in-process only and registers no listener.

Health Observation Boundary (Milestone 98d)

Health observations extend the M98a/M98b pattern to the health plugin. HealthPlugin({ diagnostics }) is OFF unless the option is passed, and an omitted option registers only an inert, disabled source under CAPABILITIES.HEALTH_DIAGNOSTICS — no capture, no timer. When enabled, the boundary is again STRUCTURAL, not a filter over captured data: the collector is handed the already-produced HealthCheckResult from the health service’s single evaluation (one callback per normal check — the report and the observation are the same evaluation), and it retains only a fixed field set per explicitly allowlisted indicator alias. An indicator’s data, the thrown value of a rejection, and any absolute time are never admitted; the projected observation carries the approved alias, the framework’s own status (present only when reported), the outcome state, and monotonic latencyMs/ageMs. The source’s snapshot(instanceId) is synchronous and frozen: it never invokes an indicator, resolves a lazy factory, or mutates state, so a slow or hostile consumer cannot backpressure the health endpoint.

The optional scheduled block is the one part that performs work: a bounded scheduler (at most 16 approved indicators, concurrency 1–4, a per-check reporting timeoutMs that is a bound — not a cancellation — so a hung check stays in-flight until it settles and no replacement starts for it; the hung callback holds its own concurrency slot and nothing more, cycles wait only for reporting, and a rotating cursor keeps every scheduled indicator reachable) is started from the plugin’s onBootstrap hook and torn down from onClose, including the failed-startup path. The M98b connector consumes the source to serve GET /v1/health, projecting the DTO field-by-field and bounding it by the same 256 KiB response ceiling; an absent source answers unsupported, a throwing source — or one whose projected DTO fails the exact validator — a value-free collection-failed, and neither changes the application’s readiness.

Configuration Provenance Boundary (Milestone 98e)

Configuration provenance extends the same pattern to the config plugin. ConfigPlugin({ diagnostics }) / loadConfig(runtime, { diagnostics }) is OFF unless the option is passed; an omitted option registers only an inert, disabled source under the EAGER CAPABILITIES.CONFIG_DIAGNOSTICS token, so the connector can distinguish “no config plugin” (unsupported) from “present but off” (disabled). When enabled, the boundary is again STRUCTURAL: the provenance builder observes only framework-owned structure at the resolution steps the loader already performs — the merge displacement chain, the existing ${NAME} grammar on an already-loaded string, and schema input/output property PRESENCE — and it never compares, hashes, serializes, measures, or retains a value. Only explicitly approved key aliases and file-path aliases are retained; unapproved keys are dropped without a count (a count would disclose that they exist), and an unapproved file path contributes only its category. The record travels with the exact configuration object through a module-private WeakMap, so the standalone loadConfig → ConfigPlugin({ instance }) composition stays ONE snapshot with ONE metadata pass, and GC owns the lifetime. An opaque injected instance is answered with unknown entries — no presence flag, no read of it; configured sections perform exactly the same IConfig.get calls with diagnostics disabled and enabled. The connector consumes the source to serve GET /v1/config with the identical projection, validation, bounding, and failure isolation as GET /v1/health.

Queue Observation Boundary (Milestone 98f)

Queue observations apply the same pattern to background work, with two structural differences. The first is multiplicity: a queue plugin can be registered several times under derived names, and the kernel refuses two plugins claiming one capability in provides, so a singleton inspector source is impossible. Every QueuePlugin instance therefore contributes one IQueueDiagnosticsSource under CAPABILITIES.QUEUE_DIAGNOSTICS with { multi: true } and never claims the token — the contribution-token pattern HEALTH_INDICATOR already uses — and the connector reads every source it finds at bootstrap (at most 16), identifying each only by a q<N> position so registry names and tokens never reach the wire.

The second is the settlement boundary. The queue’s existing outcome notification (the metrics sink) fires BEFORE the adapter’s settlement call is awaited, so it is not proof that a job was durably acknowledged, requeued or dead-lettered. The runner therefore has two hooks: the metrics hook keeps its timing, and the diagnostics hook fires only after the settlement promise settled — with the requested settlement on resolution, failed on rejection (which is then rethrown unchanged) — and the collector records unknown in place of a completed call on an adapter that cannot confirm its settlement calls (RabbitMQ’s unconfirmed channel operations; SQS, which absorbs a lapsed claim or a failed dead-letter send). Minimization is again structural: the observer is handed a job name (for the exact allowlist lookup), a raw job id (for the bounded LRU alias lookup) and the attempt number at dispatch, and fixed outcome/settlement primitives afterwards — its signatures cannot accept a payload, a header, a claim token, the attempt limit or a thrown value. Everything retained is bounded (1,024 attempts, 4,096 aliases, 2,048 observed in flight, 64 latest depths).

Depths are counted only by a separately opted-in scheduler (non-overlapping cycles, concurrency 1–4, a reporting timeoutMs that never cancels — a hung count keeps its slot until it settles), and only on adapters that can count cheaply; an adapter that cannot reports unavailable, never zero, and each depth carries its scope (process-local or shared-backend) so a consumer never sums one backend’s inventory across replicas. A diagnostic read never reserves, settles or counts a job.

The connector keeps one internal cursor per source (M98b permits one session), drains each source’s new attempts into a bounded merge ring on every authenticated read, and pages that ring under M98a’s cursor contract. Two rings mean two losses, reported separately: merge-ring eviction in the batch’s lost, and a source ring that wrapped between two reads on that source’s status, so an unbroken merge sequence never reads as complete coverage. A source is untrusted input — any installed plugin can contribute one — so its batch is validated key-by-key before it is merged, and a failing source is isolated as collection-failed without breaking the read.

Trace Observation Boundary (Milestone 98g)

Trace observations apply the pattern to the telemetry plugin, whose structural difference is the observation SEAM: spans are produced by OpenTelemetry, and the framework does not own a completed -span feed. Rather than a competing tracer or a wrapper around the exporter, the plugin appends one internal SpanProcessor AFTER the exporter processor in the SAME BasicTracerProvider constructor — the exporter path is untouched (measured: it still receives every span), the diagnostic processor never exports, forceFlush resolves without touching the exporter, and shutdown closes the collector only when the provider shuts down, after the connector session’s onStopping revocation. Minimization happens IN the processor, before retention: onEnd reads only name, kind, span context, parent context, link contexts, status code and duration; the exact raw-name allowlist replaces the name with its approved alias before anything is buffered; attributes, events, resources, tracestate, baggage, exceptions and status messages are structurally unreachable. Any failure — a hostile getter, an unmappable kind or status, an invalid identifier — is one saturating drop; the processor never throws into OTel.

Kinds follow the @opentelemetry/api SpanKind enum a readable span carries — INTERNAL = 0, SERVER = 1, CLIENT = 2, PRODUCER = 3, CONSUMER = 4 — the numbering both auto-instrumentation and the framework’s TelemetryService write (a default span arrives as 0, measured on the locked SDK); the OTLP wire enum is this plus one and never reaches the processor. An unmappable value drops the record rather than improvising. The collector is the single source under CAPABILITIES.TRACE_DIAGNOSTICS (one telemetry plugin per application, so no merge ring is needed), always registered: disabled without the option, unsupported with the fixed coverage reason under a custom provider factory or in noop mode. Parent and link relationships are identifier relationships only — parentVisibility names whether the parent is locally observed, unobserved, a root, or unknown — and no global clock is implied: ageMs is arrival age at one process. Sampling stays authoritative: unsampled spans never complete through a processor, and the batch reports the configured sampler so partial traces stay visible as partial.

Cache observations (M98i) follow the queue pattern’s multi-provider registration without its merge ring: each CachePlugin instance attaches a bounded collector to its OWN CacheService through a private field set by an internal, non-barrel attach function (the exported constructor is unchanged, and an unattached service does one field read beyond the pre-M98i path) and registers an ICacheDiagnosticsSource under CAPABILITIES.CACHE_DIAGNOSTICS with { multi: true }. Only a fixed operation name, a primitive outcome code and at most two monotonic readings cross into the collector — the wrapper classifies a result (=== null for get, === true for has/delete) before calling it, and a rejection is recorded without the error being read. The connector resolves the sources once at bootstrap (refusing more than 16), reads each synchronously only after authentication, and copies only own data properties of plain objects, so a hostile replacement source cannot run a getter or smuggle a field. Coverage is owned-instance: direct store calls and replacement services are not represented, and no eviction is inferred from misses. The caller receives a promise derived from the backend’s, which re-rejects with the ORIGINAL reason, so an unhandled backend rejection stays unhandled with diagnostics on (a side branch on the caller’s own promise would have marked it handled and hidden it). Only one call in eight per operation is timed with a start reading — the per-call cost is one clock read plus one promise reaction.

Scheduler execution observations (M98k) use the same multi-provider shape, attached to the plugin’s OWN SchedulerService and threaded to the executor per dispatched fire. The service observes each approved job’s timer fire at its entry — lateness measured once against the intended epoch fire time (max(0, actualStart - intendedFire), never an absolute schedule) — and settles it at the skip-or-dispatch decision: contended (fire slot held elsewhere, a delay whose registration slot belonged to another replica, or the overlap mutex held), lock-failed (a lock operation rejected), or dispatched with its settlement and monotonic duration. The executor observes handler attempts — started, succeeded/failed, the retry count — reading clocks only when an observer was supplied, so an unapproved job allocates nothing and a lock loser produces NO attempt record. A skipped local fire is never a globally missed execution: no missed counter exists, and coverage is always owned-instance, never cluster completeness. Storage observations (M98m) reuse the cache pattern without change of shape: each StoragePlugin instance attaches a bounded collector to its OWN StorageService through a module-private WeakMap written by an internal, non-barrel attach function (the exported constructor is unchanged, and an unattached service does one WeakMap read beyond the pre-M98m path) and registers an IStorageDiagnosticsSource under CAPABILITIES.STORAGE_DIAGNOSTICS with { multi: true }. Only a fixed operation name, a primitive outcome code, at most two monotonic readings and a byte length cross into the collector — the wrapper classifies a result (a null get is impossible because absent objects throw; a false exists/delete is a success) before calling it, and a rejection is recorded without the error being read. getSignedUrl mints a URL rather than transferring bytes, so the collector carries null for its duration and byte length by construction; getStream counts the open, not the drain. The collector is NOT shared in common (unlike the M98l realtime collector) because a single plugin owns it; the option and collector types live in @setu-ts/storage-plugin. The connector resolves the sources once at bootstrap (refusing more than 16), reads each synchronously only after authentication, and copies only own data properties of plain objects, so a hostile replacement source cannot run a getter or smuggle a field. Coverage is owned-instance: direct provider calls and a service constructed without the collector are not represented. Object paths, stored bytes, content types, signed URLs (the synthetic memory:// URL encodes the path) and error text never reach the collector, the wire, or the client. The caller receives a promise derived from the provider’s, which re-rejects with the ORIGINAL reason, so an unhandled provider rejection stays unhandled with diagnostics on.

Authorization Decision Explanation Boundary (Milestone 98h)

Authorization decision explanations apply the pattern to the first-party RBAC evaluator, with two structural differences that make the boundary stricter than every inspector before it.

The first is the observation SEAM. The RbacService is framework-owned, but its public surface is four synchronous boolean methods (IAuthorizationService) that every guard, decorator middleware and direct consumer calls — and the boolean IS the answer. There is no side channel to hook: the evaluation must be refactored so that the SAME code path that produces the boolean also emits one guarded decision observation, and the refactor is proven by the guards’ byte-identical status/body and short-circuit behaviour. The observer is stored in a package-private WeakMap keyed by the RbacService instance and attached only when the authorizationDiagnostics option is passed, so a JWT-only registration and a registration with RBAC but no option are both unobserved: the evaluator builds no step list and invokes nothing when no observer is attached. Minimization is structural: the evaluator hands the observer only the requested rule names, the fixed reason, the granting role name and the step it just ran — its signatures cannot accept a principal, a request, a credential or an error — and the collector replaces every name with its approved alias before anything is retained, so a raw rule name never reaches the ring. A compound that short-circuits emits exactly the steps it evaluated (at most 16 retained, the true count in stepsEvaluated, stepsTruncated true when more ran), never a fabricated full sweep; a decision whose requested rule lacks an approved alias is dropped BEFORE buffering and counted in a saturating droppedUnapproved counter, so partial rule lists are never emitted; an unapproved granting role is omitted from viaRoleAlias rather than dropping the decision.

The second is the TRUST boundary. The CAPABILITIES.AUTHORIZATION provider is replaceable — an application may register its own IAuthorizationService after the AuthPlugin, and the guards will enforce through it. The observer can therefore see a decision it did not produce, and a decision it did produce may no longer be authoritative. The source never guesses: before buffering it checks, and before every read it re-checks, that the current provider is still the exact RbacService instance it created, through the new OPTIONAL IServiceRegistry.isCurrent?(token, instance) identity predicate — non-resolving (it never instantiates a lazy factory), non-enumerating, and returning a boolean only. A registry without the predicate latches the source to unsupported/provider-identity-unavailable; a false result latches custom-provider. Both latches are terminal: the source detaches, clears retained state, and never resumes even if startup code later restores the old registration. Keeping the predicate optional means third-party registry-shaped implementations keep compiling, and a consumer that needs a current-provider check must treat its absence as “cannot verify” rather than falling back to a resolving get.

The connector consumes the source to serve GET /v1/authorization with the identical projection, validation, bounding, and failure isolation as every other inspector; the full wire shape is in docs/diagnostics-protocol.md.

Event Observation Boundary (Milestone 98j)

Event observations apply the pattern to the events plugin, whose structural differences are the observation SEAM and the hot path. The seam is the existing dispatch: a module-private WeakMap attaches the collector to the bus during plugin registration — no extra subscription (which would change dispatch and count itself), no wrapper around the bus, and no exported attachment helper. The collector accepts only the fixed operation (publish/handler), an already-approved alias, and primitive outcomes with measured durations; event payloads, event ids, aggregate ids and handler identities are structurally unreachable from its signature. Publication and each existing handler’s await are counted separately — async completion is not handler completion — publishBatch counts its constituent publishes, and a thrown errorHandler keeps its exact propagation while the observation still lands.

The hot path pays one failing WeakMap probe when detached; when attached it derives nothing per event: the exact-type lookup happens first (an unapproved type costs one probe and no clock read), then the alias replacement happens before anything is retained. Retention is the shared shape — 64 slots keyed (alias, operation), 60-second expiry checked during update and read, 30-second stale threshold, saturating counters — and a clock or observer failure latches collection-failed and stops capture without changing the application’s error. Every EventsPlugin instance contributes one IEventDiagnosticsSource under CAPABILITIES.EVENTS_DIAGNOSTICS with { multi: true } and never claims the token (the M98f contribution pattern); close detaches the observer FIRST, then clears the bus, so a late observation after shutdown is discarded rather than resurrecting state. The connector reads every source at bootstrap (more than 16 refuses startup; s<N> position ids; an over-budget body collapses to collection-failed), isolates a failing source as collection-failed, and answers duplicate aliases with a fixed no-source response.

Realtime observations (M98l) apply the pattern to three packages at once — the WebSocket, SSE and realtime-backplane plugins — which is what put their collector in @setu-ts/common: the three need the identical collector and none may import another (the M47 frame-codec precedent), so createRealtimeObservationCollector exists once instead of three times. Each plugin attaches it to its own component through a package-private WeakMap and registers only its frozen, snapshot-only source facade — registering the collector would hand every registry reader its observe and close. Capture sits where the outcome is already known: a WebSocket send at WebSocketConnection.send (so a room broadcast or heartbeat is counted once, at the connection), an SSE send at the connection’s one enqueue path with its backlog guard reported as a fixed backpressure category, a backplane publish around each transport’s publish(), and a receive after the transport’s own shape and origin filters. Only fixed operations and booleans cross the seam. The one structural addition is the gauge: current connection and group counts are not operation records and must not expire with them, so each websocket and sse source captures a reader over its OWN service’s size getters at registration and calls it only inside an authenticated read. Nothing resolves the capability on a read, so a replacement provider is never invoked, and close releases the reader.

Outbound HTTP observations (M98n) are the one letter whose producer is not a plugin: the SDK is a browser-portable library whose only common imports are types. createObservedFetch therefore returns a fetch wrapper together with a small registration plugin that puts the helper’s frozen, snapshot-only source under the multi token CAPABILITIES.OUTBOUND_HTTP_DIAGNOSTICS (written in the SDK as a literal, pinned to CAPABILITIES by a test), and its collector is a deliberate local copy rather than an import from common. The wrapper forwards the caller’s own arguments and receiver to exactly one delegation and reads only the response’s status. The documented composition builds the helper only when the application factory receives its devtool composition, so a production entry carries no wrapper at all.


15. Performance Philosophy

Minimal Allocations

The framework minimizes object allocations:

  • Request context is reused where possible (pooled in high-throughput scenarios).
  • Middleware chain is pre-compiled, not rebuilt per request.
  • Route matching uses Hono’s TrieRouter, compiled once at registration, with candidate ranking.
  • Service instances are cached (singleton scope by default).

Lazy Loading

graph LR
    App[App Start] --> Kernel[Kernel Init]
    Kernel --> Plugins[Plugin Registration]
    Plugins -->|lazy| Services[Service Factories]
    Services -->|on first get| Instance[Service Instance]
  • Plugins register factories, not instances.
  • Service instances are created on first get().
  • Heavy services (database, message broker) are only initialized when first used.

Plugin Loading

Plugin loading is optimized:

  1. Topological sort — O(V + E) where V is plugins and E is dependencies.
  2. Parallel registration — Plugins with no dependencies between them can register in parallel (future optimization).
  3. No reflection — Metadata is stored in plain objects, no runtime reflection overhead.

Caching

  • Route matching results are cached per request.
  • Service instances are cached (singleton scope).
  • Config values are cached after first read.
  • OpenAPI spec is generated once and cached.

Tree Shaking

Every package is tree-shakeable:

  • ES module exports only.
  • Subpath exports for granular imports.
  • No top-level side effects.

Startup Optimization

  • Plugins register factories, not instances.
  • The middleware pipeline is compiled once at startup.
  • The route table is built once at startup.
  • Config validation happens once at startup.

Runtime Performance

  • Hono router for fast route matching.
  • Minimal middleware overhead (each middleware is a function call).
  • No reflection in the hot path.
  • Async/await throughout (no callback overhead).

Memory Efficiency

  • No global state.
  • Request context is garbage-collected after response.
  • Service singletons are the only long-lived objects.
  • Event listeners are cleaned up on shutdown.

16. Testing Strategy

Test Pyramid

graph TB
    E2E[E2E Tests - Few]
    Integration[Integration Tests - Some]
    Unit[Unit Tests - Many]

    E2E --> Integration
    Integration --> Unit

Unit Tests

  • Test individual functions and classes in isolation.
  • Mock all dependencies.
  • Fast execution (< 100ms per test).
  • 90%+ coverage required.

Integration Tests

  • Test plugin registration and service resolution.
  • Test middleware pipeline execution.
  • Test route matching and handler execution.
  • Use in-memory adapters (no real databases, Redis, etc.).

Plugin Tests

Each plugin must test:

  1. Registration — Plugin registers correctly with the kernel.
  2. Service resolution — Services are resolvable by capability token.
  3. Middleware — Plugin middleware executes correctly.
  4. Routes — Plugin routes are registered and matchable.
  5. Lifecycle — Lifecycle hooks are called correctly.
  6. Error handling — Plugin handles errors gracefully.
  7. Optional dependencies — Plugin works when optional dependencies are absent.

Runtime Compatibility Tests

  • Deno runs the full workspace test suite against the working tree (HEAD).
  • Node and Bun run the published-artifact compatibility suite in compat/, which consumes the packages through JSR’s npm compatibility layer — it exercises module loading, runtime detection, and the per-runtime HTTP adapters, not the current HEAD source.
  • Runtime-specific tests are guarded with runtime.platform() checks.

Contract Tests

  • OpenAPI spec is validated against the actual API.
  • Route schemas match the actual request/response shapes.
  • Plugin interfaces are validated against their implementations.

End-to-End Tests

  • Test full application scenarios.
  • Use app.inject() for HTTP testing without a server.
  • Test the complete request lifecycle.
  • Test graceful shutdown.

Test Utilities

The @setu-ts/testing package provides:

  • createTestApp() — Create a test application with specified plugins.
  • createMockPlugin() — Mock a plugin’s service.
  • inject() — Send HTTP requests without a server.
  • createTestContext() — Create a mock request context.
  • MockServiceRegistry — Mock service registry implementation.
  • MockResponse — Mock response builder.
  • FixtureManager — Manage fixture setup and reset.
  • collectStream() — Read streaming response bodies incrementally.

17. Extending the Framework

Creating a Plugin

import type { IPlugin, IPluginContext } from '@setu-ts/common';

export function MyPlugin(options: MyPluginOptions): IPlugin {
  return {
    name: 'my-plugin',
    version: '1.0.0',
    dependencies: ['logger'],
    provides: ['my-capability'],
    register(ctx: IPluginContext) {
      const logger = ctx.services.get('logger');

      // 1. Register a service
      ctx.services.register('my-capability', new MyService(options));

      // 2. Add middleware
      ctx.middleware.add(myMiddleware(), { priority: 300 });

      // 3. Register routes
      ctx.router.get('/my-route', (ctx) => {
        const service = ctx.services.get<MyService>('my-capability');
        return ctx.response.json(service.getData());
      });

      // 4. Register health check
      ctx.health.register('my-service', async () => ({
        status: 'up',
        data: { version: options.version },
      }));

      // 5. Register metric
      ctx.metrics.register('my_operations_total', {
        type: 'counter',
        help: 'Total my operations',
      });

      // 6. Register CLI command
      ctx.cli.register('my:command', () => {
        console.log('Running my command');
      });

      // 7. Register lifecycle hook
      ctx.lifecycle.onShutdown(() => {
        logger.info('My plugin shutting down');
      });
    },
  };
}

Creating a Runtime Adapter

import { IRuntimeServices, RuntimePlatform } from '@setu-ts/common';

export class MyRuntimeServices implements IRuntimeServices {
  platform(): RuntimePlatform {
    return 'my-runtime';
  }

  version(): string {
    return '1.0.0';
  }

  uuid(): string {
    // Implement UUID generation for your runtime
  }

  // ... implement all IRuntimeServices methods
}

Creating Custom Middleware

function myMiddleware(options?: MyOptions): MiddlewareFunction {
  return async (ctx, next) => {
    // Pre-processing
    const start = ctx.services.get<IRuntimeServices>('runtime').now();

    await next();

    // Post-processing
    const duration = ctx.services.get<IRuntimeServices>('runtime').now() - start;
    ctx.response.header('X-Duration', duration.toString());
  };
}

Creating a Custom Decorator

import { Controller, createDecorator, Get } from '@setu-ts/decorator-plugin';

export const Cacheable = (ttl: number) => createDecorator('cacheable', { ttl });

// Usage
@Controller('/api')
class ApiController {
  @Get('/data')
  @Cacheable(3600)
  async getData() {
    return this.service.getData();
  }
}

Creating a Database Adapter

IDatabaseAdapter is the full backend port — lifecycle plus data access. It lives in common (promoted there in M52c) precisely so a backend can be implemented in any package, including one outside this repository, without importing database-plugin. Register it through the plugin’s 'custom' arm.

There is no migrate() on the contract: programmatic migration is not honestly implementable across the supported backends, and each one owns migrations through its own CLI.

import type { IAdapterTransaction, IDatabaseAdapter, IDataSource } from '@setu-ts/common';

export class MyAdapter implements IDatabaseAdapter {
  async connect(): Promise<void> {
    /* ... */
  }
  async disconnect(): Promise<void> {
    /* ... */
  }
  isReady(): boolean {
    /* ... */
  }

  // One data source per entity; it owns query evaluation end to end.
  createDataSource(entity: string): IDataSource {
    /* ... */
  }

  // The handle also opens transaction-scoped data sources, which is what makes
  // a Unit of Work spanning several entities atomic.
  async beginTransaction(): Promise<IAdapterTransaction> {
    /* ... */
  }

  async rawQuery<T>(sql: string, params?: unknown[]): Promise<T[]> {
    /* ... */
  }
}

app.register(DatabasePlugin({ type: 'custom', adapter: new MyAdapter() }));

@setu-ts/cloudflare-plugin’s D1Adapter is the worked example.

Creating a Cache Adapter

import { ICacheStore } from '@setu-ts/common';

export class MyCacheStore implements ICacheStore {
  async get<T>(key: string): Promise<T | null> {
    /* ... */
  }
  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    /* ... */
  }
  async delete(key: string): Promise<boolean> {
    /* ... */
  }
  // ... implement all ICacheStore methods
}

Creating a Transport Adapter

Transport adapters (message brokers, queue systems) follow the same pattern:

import { IMessageBroker } from '@setu-ts/common';

export class MyMessageBroker implements IMessageBroker {
  async connect(): Promise<void> {
    /* ... */
  }
  async disconnect(): Promise<void> {
    /* ... */
  }
  async publish<T>(topic: string, message: T): Promise<void> {
    /* ... */
  }
  async subscribe<T>(topic: string, handler: MessageHandler<T>): Promise<Subscription> {
    /* ... */
  }
}

18. Future Evolution

How the Architecture Supports Evolution

graph TB
    subgraph Stable Core
        Kernel[Kernel]
        Common[Common]
        Runtime[Runtime]
    end

    subgraph Evolving Plugins
        Existing[Existing Plugins]
        New[New Plugins]
        Replaced[Replaced Plugins]
    end

    subgraph Future
    end

    Kernel --> Existing
    Kernel --> New
    Kernel --> Replaced

The WebSocket Plugin shipped in Milestone 46 and is no longer a future extension point. The plugin does not hang off the kernel: it depends on common and the http-adapter capability, because an RFC 6455 handshake needs the native Request and answers with a 101 carrying a socket — neither of which the kernel’s IRequest/IResponse abstraction can express, and the handshake itself stays an HTTP-adapter concern reached through the optional IHttpAdapter.setUpgradeRouter seam. Deciding the upgrade does not, though, and has not since M70a: the kernel’s terminal handler resolves IWebSocketService from the registry and calls routeUpgrade after the §10 pipeline has run and before route matching, then records an upgrade intent for the adapter to act on. The pipeline is therefore not bypassed for upgrade requests — that was the pre-M70a behaviour, and it is what let an unauthenticated socket through a guarded endpoint.

The gRPC Plugin shipped in Milestone 49 and is likewise no longer a future extension point. It depends on common alone — it needed the http-adapter capability when it installed a handler there, and since M70a it installs nothing and resolves no adapter. A gRPC exchange cannot travel through IRequest/IResponse — the former exposes no raw streaming body and the latter no trailing headers. RPC is dispatched from the kernel’s terminal handler rather than intercepted in the adapter: the kernel resolves IGrpcService, asks claims() whether the path belongs to gRPC, and calls handleRequest with a Request rebuilt from the already-mapped body — a path gRPC does not claim falls through to route matching untouched. IHttpAdapter.setRpcHandler is deprecated and consulted by nothing. Only the server shipped: gRPC/Connect clients are generated by the consuming application’s own toolchain, and the browser client story belongs to @setu-ts/sdk (Milestone 35).

The GraphQL Plugin shipped in Milestone 51 and is no longer a future extension point. Unlike its two neighbours above it needs no adapter seam at all, and that negative result is the point: a GraphQL-over-HTTP exchange is a POST of application/json answered with JSON, so every byte of it fits IRequest.json() and IResponse.send() as committed. The plugin registers two ordinary kernel routes (POST and GET at path, default /graphql), rides the normal middleware pipeline, and widens neither IHttpAdapter nor any runtime. Do not copy the setUpgradeRouter/setRpcHandler pattern by analogy — reach for an adapter seam only when the exchange genuinely cannot be expressed as IRequest/IResponse.

Milestone 51b added the two long-lived transports, and neither needed an adapter seam either. Subscriptions ride the OPTIONAL CAPABILITIES.WEBSOCKET — the plugin registers a graphql-transport-ws route through IWebSocketService.route(), so M46 owns the upgrade and the GraphQL plugin owns only the protocol state machine — and GraphQL-over-SSE in distinct-connections mode, which is an ordinary kernel route returning IResponse.stream() and needs no other plugin at all. Both are opt-in: with no subscriptions option neither route exists, and POST/GET /graphql still answers a coded 400 for a subscription operation. The one widening the transports did force is WebSocketRouteOptions.heartbeat, because websocket-plugin’s heartbeat sweeper is global across routes and its raw text frame is not a protocol message: a route that speaks its own liveness protocol has to be able to leave the sweep. cloudflare-plugin and graphql-plugin are therefore both examples of the same rule — widen a committed option when a second, legitimate consumer needs it, rather than duplicating the mechanism.

Versioning

  • Kernel — Extremely stable. Breaking changes only in major versions.
  • Common — Extremely stable. Interface additions are minor versions; removals are major.
  • Plugins — Independently versioned. Each plugin follows semver.
  • Starters — Versioned as bundles. A starter version pins compatible plugin versions.

Backward Compatibility

The architecture ensures backward compatibility through:

  1. Capability tokens — Tokens are strings. New tokens can be added without breaking existing ones.
  2. Optional plugins — New plugins do not affect applications that do not use them.
  3. Interface additions — New interface methods can be added with default implementations.
  4. Plugin replacement — A new plugin can replace an old one by registering the same token.

Plugin Evolution

Plugins can evolve independently:

  • A plugin can add new features in minor versions.
  • A plugin can deprecate old APIs with @deprecated JSDoc.
  • A plugin can remove deprecated APIs in major versions.
  • A plugin can be replaced by a new plugin with the same capability token.

Capability Evolution

New capabilities can be added without affecting existing ones:

// v1.0: Existing capability
ctx.services.register('database', new DatabaseService());

// v2.0: New capability added
ctx.services.register('vector-database', new VectorDatabaseService());

// Existing applications are unaffected

Public API Stability

The public API (defined in PUBLIC_API.md) is stable:

  • No breaking changes in minor or patch versions.
  • Deprecation period for removed APIs.
  • Major version bumps for breaking changes.
  • PUBLIC_API.md is the source of truth and is updated with every API change.

Future Additions

The architecture supports future additions without breaking existing applications:

Future AdditionHow It Fits
New Database AdapterNew plugin that provides the database token with a new ORM
New Message BrokerNew plugin that provides the messaging token with a new broker
New Auth StrategyNew plugin that extends the auth-plugin with a new strategy
New Health IndicatorNew plugin that registers a health check
New Metric CollectorNew plugin that registers a metric collector

Migration Paths

When breaking changes are necessary:

  1. Announce — Document the breaking change in a release notes and PUBLIC_API.md.
  2. Deprecate — Mark the old API as @deprecated with a migration path.
  3. Provide replacement — Offer a new API that replaces the old one.
  4. Wait — Allow at least one minor version for migration.
  5. Remove — Remove the deprecated API in the next major version.

19. Deployment

The framework’s deployment model follows directly from the runtime abstraction in §4: a Setu-TS application is an ordinary process that binds a socket, so it containerizes like any other, and Cloudflare Workers — which has no listen() — deploys through its own platform instead.

Operator-facing detail lives in docs/deployment.md; this section records only the architectural constraints.

19.1 Images build from the repository root

An application is a member of the pnpm workspace and depends on @setu-ts/* as workspace:^. Only the root pnpm-workspace.yaml and pnpm-lock.yaml let a frozen install link those members, so a container image must carry both alongside packages/; without the lock, pnpm install --frozen-lockfile refuses to run. This is why the build context is the repository root rather than the application directory. The image then compiles the members it needs and copies the app out with pnpm deploy --prod, so it runs the packed framework (dist/) rather than workspace source.

The no-shell variant is a Node single executable application on distroless/cc-debian12. It is 211 MB — 4.7× the 44.9 MB Deno-compiled image it replaced — because it embeds the Node runtime; its reason to exist is the removed shell and package manager, not size. The binary links against glibc, so distroless/cc is the smallest base it runs on.

19.2 Health endpoints are the orchestration contract

HealthPlugin registers /health, /live and /ready. The latter two are what a Kubernetes livenessProbe and readinessProbe consume, which makes them part of the deployment contract rather than a convenience: an application that does not register the plugin cannot express readiness to an orchestrator at all, and will never pass a rollout.

19.3 Graceful shutdown is the application’s responsibility

ILifecycleApi.onStopping runs at the start of stop(), before the socket closes, so a service can deregister while it is still reachable. Nothing calls stop() on SIGTERM automatically. The framework installs no signal handler: doing so would be a side effect at import time (§11.5 of the guidelines) and signal APIs are runtime-specific (§4.1), so it belongs to application code, which is why stop() is a public programmatic API. Applications that skip it are killed outright and run no shutdown hooks.

19.4 Boundary with service discovery

This layer owns the platform objects — the Kubernetes Service, the EndpointSlices it generates, and the RBAC a pod needs to read them. Resolving a name to instances, balancing across them, watching for changes and ejecting outliers belong to @setu-ts/service-discovery-plugin. The split matters because the plugin cannot grant itself the permission it needs; that grant is a deployment artifact.


Conclusion

The Setu-TS architecture is designed for longevity, replaceability, and runtime portability. By making everything a plugin, using capability tokens for communication, and abstracting runtime-specific APIs, the framework can evolve without breaking existing applications.

Contributors should internalize these principles:

  1. Everything is a plugin — If you are adding a feature, it should be a plugin.
  2. Communicate via tokens — Never import from another plugin; use the service registry.
  3. Abstract the runtime — Never use runtime-specific APIs outside the runtime package.
  4. Prefer composition — Use factory functions, not inheritance hierarchies.
  5. Document everything — JSDoc on every public API; update PUBLIC_API.md for API changes.
  6. Test everything — 90%+ coverage; tests on all runtimes.
  7. Never break backward compatibility — Deprecate, do not remove.

For implementation phases, refer to ROADMAP.md. For developer-facing API, refer to PUBLIC_API.md. For engineering rules, refer to AI_GUIDELINES.md.