Skip to content

Independent R&D project · Cologne

TypeScript and Node

Runtime schemas as the source of types, framework validation defaults, AsyncLocalStorage and dependency rules.

Non-normative

Companion version
1.0
Maps to COADF Core
2.2
Status
Current
Last reviewed
Profile version
1.0
Code examples
Code examples follow in a later revision.

Code examples follow in a later revision.

Architectural properties addressed

  • P-1A runtime schema as the contract, the static type derived from it, and, in the reference pattern, a factory as the only way to a verified value.
  • P-4OpenTelemetry context through AsyncLocalStorage, and the audit identity as a required field of every message and job.
  • P-5Provenance declared in the schema, and preserved by the response serialiser.
  • P-6Dependency rules and schema checks in continuous integration, each with a planted failure.
  • P-7Ports in the domain package; one adapter package per external system.
  • P-8A policy interface over an engine, over HTTP or compiled to WebAssembly, with the revision on every decision.
  • P-2, P-3Only at the depth COADF publishes.

Architectural intent

TypeScript gives the boundary a vocabulary and none of its enforcement. Type annotations never change the runtime behaviour of a program: they describe what a value should be and vanish before it runs, while the data arriving from a model, a queue or a request has whatever shape it has. The standard typings declare the result of JSON.parse as any, which the compiler then lets you treat as anything.

So in this ecosystem the boundary is the runtime schema, and the static type is derived from the schema, never the other way round. Everything else in this profile follows from that inversion. Zod, Ajv, Fastify and NestJS are examples: COADF requires none of them, and any runtime validator that refuses what the contract does not declare keeps the property.

Technology mapping

TypeScript and Node: Technology mapping
Architectural propertyTypeScript and Node
Boundary typeA runtime schema (Zod, or JSON Schema with Ajv) as the source, with the static type inferred from it. TypeScript's type system is structural, so a verified value that must not be confused with a proposal needs a brand, and only a factory applies it.
Runtime validationparse or safeParse at every receiving side. Zod's parse throws on invalid data and safeParse returns a result instead.
Unknown keysAn explicit choice per schema. In Zod 4, z.object() strips unknown keys and z.strictObject() rejects them.
Framework validationFastify route schemas, validated with Ajv and serialised with fast-json-stringify; or the NestJS ValidationPipe with whitelist and forbidNonWhitelisted.
Architecture ruleDependency rules checked in CI: dependency-cruiser, ESLint boundary rules, or TypeScript project references that make an illegal import a compile error.
Execution contextOpenTelemetry JS, whose Node context manager is based on AsyncLocalStorage.
Audit trailThe same PostgreSQL schema through a driver; entries written in the business transaction.
Standards isolationPort interfaces in the domain package; each vendor client confined to an adapter package in the workspace.
PolicyOpen Policy Agent over HTTP, or Rego compiled to WebAssembly and evaluated in process through OPA's JavaScript SDK; the revision on every decision.

Reference pattern

Code examples follow in a later revision. They are deferred until a reviewer working in this ecosystem has read them, rather than written to make the four profiles look alike. The structure below is the reference pattern those examples will implement.

Packages

  • schemas holds the runtime schemas of every contract: the proposal with its provenance, the audit entry with the published fields, the policy decision with its revision. Every other package imports its types from here, inferred from the schemas.
  • inference calls the model and returns the result of parsing the reply with the proposal schema. It cannot import records or audit, and a dependency rule in CI says so.
  • records exports a factory that takes a proposal and the verification of a named person and returns a branded verified value. The brand's type is not constructible elsewhere without a cast, and a lint rule forbids that cast outside this package.
  • audit, classification with its vendor adapter, and policy follow the same shape as in the other profiles.

Boundaries at run time

Every receiving side parses: the HTTP route, the message consumer, the job runner and the domain factory. A route schema in Fastify or a pipe in NestJS covers the web entry point only.

The OpenTelemetry SDK is loaded before the application's own modules. An ES module application needs OpenTelemetry's loader hook for automatic instrumentation, in addition to preloading the SDK.

Failure modes

  1. A type annotation mistaken for validation

    Casting the result of JSON.parse to the proposal type compiles, and checks nothing. The cast is the most common boundary in TypeScript code, and it is not a boundary.

  2. Stripping mistaken for refusing

    A default Zod object strips unknown keys. A reply that claims to be verified is not refused; the claim is quietly removed, and the fact that the model over-claimed is lost with it. Where the claim itself is evidence, use a strict object and record the rejection.

  3. Coercion by default in Fastify

    Fastify's default Ajv options include coerceTypes, useDefaults and removeAdditional. A number sent as text arrives as a number, and a missing value may arrive as a default. Configure the validator for boundary routes explicitly.

  4. Provenance dropped by the response serialiser

    With a response schema, properties the schema does not list are left out unless it allows additional properties. A response schema written for the screen, without the provenance, removes it from every answer.

  5. A NestJS pipe that passes everything through

    whitelist strips properties without validation decorators, and forbidNonWhitelisted throws instead of stripping. Without either, a property the DTO does not declare reaches the handler.

  6. Context lost across asynchronous work

    AsyncLocalStorage keeps data for the lifetime of a request or other asynchronous duration, and it follows the continuations Node can see. Work that re-enters from a pooled callback or a library's own queue can arrive without it; the audit identity has to be in the message, not recovered from the context.

  7. Automatic instrumentation silently absent

    An ES module service started without the loader hook runs normally, and the libraries the hook would have instrumented produce no spans. Nothing fails; the trace is just shorter than the execution.

  8. A brand forged with a cast

    A cast to the verified type compiles anywhere. The brand plus factory is a convention the compiler cannot enforce on its own; the lint rule and code review are what make it hold.

  9. Two versions of the shared schemas

    Producer and consumer depend on different releases of the schemas package, and each validates correctly against a different contract. Contract tests across the versions actually deployed catch it; unit tests do not.

Verification

  • Unit test

    Passes when: Malformed replies are rejected by the proposal schema: unknown key, wrong type, missing provenance, unexpected attribute.

    Proof of teeth: Replace the strict object with a default one in a scratch branch: the unknown-key test fails.

  • Unit test

    Passes when: A type-level test asserts that a proposal is not assignable to a verified value.

    Proof of teeth: Remove the brand: the type-level test fails to compile.

  • Architecture test

    Passes when: The dependency rules pass: no import from inference into records or audit, no vendor client outside its adapter.

    Proof of teeth: Plant the forbidden import: CI fails.

  • Contract test

    Passes when: The response of every route that serves a proposal contains the provenance fields.

    Proof of teeth: Remove a provenance property from the response schema: the test fails, because the serialiser dropped it.

  • Integration test

    Passes when: With an in-memory span exporter, asynchronous work shows up in the request's trace, and the persisted audit entries carry one identity.

  • Integration test

    Passes when: Policy decisions carry the revision of the policy material that produced them; answers without one are refused.

Alternative realizations

  • Other schema libraries (Valibot, TypeBox with Ajv, io-ts). The property is a runtime schema that is the source of the type; the library is a choice.
  • Schema-first transports such as GraphQL or Protocol Buffers, where the contract is a separate artifact both sides are generated from.
  • A policy engine in process through WebAssembly, instead of over HTTP, to remove the network hop; the revision requirement is unchanged.

Trade-offs

  • Schema-first moves the source of truth out of the type declarations, which some teams find unnatural; type-first leaves the runtime unguarded.
  • Compiled validators are fast and carry defaults that coerce and fill; the fastest configuration is not the strictest one.
  • Strict objects reject more of what a model produces, and every rejection needs a decision about what to record.
  • A workspace of small packages makes dependency rules expressible, and costs build and release machinery.

Limitations

  • No code is published for this profile in Companion 1.0, and no example was executed. The behaviour described is taken from the current first-party documentation, checked on 11 September 2026, for Zod 4, Fastify 5, NestJS, Node.js 24 and OpenTelemetry JS 2.
  • Nothing here shows how confidence is represented, when review is required, or how review is organised; COADF does not publish those parts of P-2 and P-3.

What this profile does not establish

Following this profile does not establish regulatory compliance, certification or conformity assessment, and neither TypeScript nor Node is required by COADF.

Tested reference environment

No example was executed for this profile.

Sources

COADF Engineering Companion 1.0 · non-normative · maps to COADF Core 2.2

Publication rights reserved. No public licence is granted for the COADF Engineering Companion 1.0 or its reference examples at this time.

IP and publication status