Code examples follow in a later revision.
On this page
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
| Architectural property | TypeScript and Node |
|---|---|
| Boundary type | A 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 validation | parse or safeParse at every receiving side. Zod's parse throws on invalid data and safeParse returns a result instead. |
| Unknown keys | An explicit choice per schema. In Zod 4, z.object() strips unknown keys and z.strictObject() rejects them. |
| Framework validation | Fastify route schemas, validated with Ajv and serialised with fast-json-stringify; or the NestJS ValidationPipe with whitelist and forbidNonWhitelisted. |
| Architecture rule | Dependency rules checked in CI: dependency-cruiser, ESLint boundary rules, or TypeScript project references that make an illegal import a compile error. |
| Execution context | OpenTelemetry JS, whose Node context manager is based on AsyncLocalStorage. |
| Audit trail | The same PostgreSQL schema through a driver; entries written in the business transaction. |
| Standards isolation | Port interfaces in the domain package; each vendor client confined to an adapter package in the workspace. |
| Policy | Open 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
schemasholds 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.inferencecalls the model and returns the result of parsing the reply with the proposal schema. It cannot importrecordsoraudit, and a dependency rule in CI says so.recordsexports 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,classificationwith its vendor adapter, andpolicyfollow 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
A type annotation mistaken for validation
Casting the result of
JSON.parseto the proposal type compiles, and checks nothing. The cast is the most common boundary in TypeScript code, and it is not a boundary.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.
Coercion by default in Fastify
Fastify's default Ajv options include
coerceTypes,useDefaultsandremoveAdditional. 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.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.
A NestJS pipe that passes everything through
whiteliststrips properties without validation decorators, andforbidNonWhitelistedthrows instead of stripping. Without either, a property the DTO does not declare reaches the handler.Context lost across asynchronous work
AsyncLocalStoragekeeps 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.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.
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.
Two versions of the shared schemas
Producer and consumer depend on different releases of the
schemaspackage, 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
inferenceintorecordsoraudit, 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
- TypeScript: The Basics: erased types · official documentation · Checked on 2026-09-11
- TypeScript: lib.es5.d.ts: JSON.parse · project repository · Checked on 2026-09-11
- TypeScript: Type compatibility · official documentation · Checked on 2026-09-11
- Zod: Basic usage: handling errors · official documentation · Zod 4 · Checked on 2026-09-11
- Zod: Objects and z.strictObject · official documentation · Zod 4 · Checked on 2026-09-11
- Fastify: Validation and serialization · official documentation · Fastify 5.12 · Checked on 2026-09-11
- Fastify: fast-json-stringify: additionalProperties · project repository · Checked on 2026-09-11
- NestJS: Validation: stripping properties · official documentation · Checked on 2026-09-11
- Node.js: Asynchronous context tracking: AsyncLocalStorage · official documentation · Node.js 24 · Checked on 2026-09-11
- OpenTelemetry: Context manager (JavaScript) · project repository · OpenTelemetry JS 2.11.0 · Checked on 2026-09-11
- OpenTelemetry JS: ESM support · project repository · Checked on 2026-09-11
- Open Policy Agent: WebAssembly · official documentation · OPA 1.20 · Checked on 2026-09-11
