/technology/architecture/ · The Harness core

Ports and adapters, composed at compile time.

Harness is a set of Rust crates. Modules ask for ports. Runtimes supply adapters. The builder checks the match before anything runs, and the compiler enforces that modules never see a vendor.

The module contract · Shipping

Six methods you must write.

A module declares its name, its version, the ports it requires, its migrations, a check on its own configuration, and an axum router. Harness::build() refuses a module that requires a port the runtime lacks, two modules on the same route prefix or table, or a module built against another contract version. That failure happens in cargo test.

The rest of the trait has defaults, so a module opts into them one at a time: optional() ports, tables(), emits(), public_writes(), well_known() for spec-mandated discovery documents outside /v1, events(), scheduled(), and surface(), which is what gives a module its pages.

crates/core/src/module.rs
pub trait Module: Send + Sync + 'static {
    fn name(&self) -> &'static str;      // /v1/<name>
    fn version(&self) -> &'static str;   // /__health
    fn requires(&self) -> &'static [Port];
    fn migrations(&self) -> Migrations;  // portable SQL
    fn validate_config(&self, cfg: &dyn Config)
        -> Result<(), ConfigError>;
    fn router(&self, ctx: ModuleContext) -> axum::Router;

    // ... and nine more with defaults, including:
    fn surface(&self) -> Surface { Surface::none() }
}

Ports

Ten trait objects. No vendor in sight.

Database
SQLite · D1 · Postgres
Mailer
Resend
Captcha
Turnstile
RateLimiter
Workers binding · Redis · in-memory
Signer
HMAC, kid rotation
KeyValue
Cloudflare KV · Redis · in-memory
HttpClient
Workers fetch · reqwest
Clock
system · fixed (tests)
IdGen
ULID
Defer
wait_until · tokio

CI proves the core stays vendor-free by building the example backend to wasm32 every commit. A dependency pulling tokio, mio or std::fs fails the build. The second column of that proof is the native runtime: the same modules on tokio, Postgres and Redis, with every module's tests run against both database engines.

There is an eleventh trait, Dispatcher, which forwards a request to a sidecar module's own worker. It is deliberately not one of the ten: it never appears in a module's requires(), because a module that could reach it could call any sidecar the venture has bound.

Request lifecycle · Shipping

requestHTTPS
axum router/v1/<name>
modulecrate
porttrait object
adapterruntime-cloudflare
D1your account

The UI surface · Shipping

The forms come from the handlers.

A module declares a Surface: the actions a renderer may offer and the views that compose them. The input schema for each action is derived from the very serde type the handler deserializes, so a form cannot drift from the route it posts to. Harness::build() validates it alongside ports and tables, and GET /__surface serves the result.

Specified in ADR 0010, built in #69–#77.

  • Pages at /ui/<module>/<action>, the same markup as a fragment with ?fragment=1
  • A form posts to its own /ui route; the handler turns it into the JSON the module already accepts and dispatches it in-process, so modules stay JSON-only
  • A problem+json comes back as the form re-rendered with the error on its field and the values kept
  • cf.js, a dependency-free embed under 4 KB gzipped with a size gate in CI, puts those forms on a static site
  • Styling is a CSS contract, not an API: cf-* classes and cf.css inside @layer cf, so a venture stylesheet wins without !important
  • Admin pages behind a token login, with table views over each module's own admin export. No Signer or no admin token means no admin UI

Why Rust

The compiler is the test you cannot skip.

Rust is not on this stack because it is fast in a benchmark. It is here because a whole class of production incident becomes a build failure, and because a backend with no garbage collector has no collection pause to schedule around: latency is what your code does, not what the runtime decided to do that second. What ships is one compiled module, not an interpreter and a dependency tree.

The table below compares language guarantees, not speed. Every row is something you can check in a compiler.

Language-level guarantees compared across TypeScript on Node, Python, Go and Rust.
Property TypeScript on Node Python Go Rust
Memory managementGarbage collectedReference counting plus a cycle collectorConcurrent garbage collectorOwnership and borrowing. Memory is released at the end of scope, by the compiler
Collection pausesYesYesShort, concurrentNone. There is no collector to pause
Memory footprintHeap headroom is reserved for the collectorHeap headroom is reserved for the collectorHeap headroom is reserved for the collectorWhat you allocate, for as long as you hold it
Absence of a valuenull and undefined, at runtimeNone, at runtimeZero values and nil; misuse panics at runtimeOption<T>. Absence is in the type, and the compiler makes you handle it
ErrorsExceptions, uncheckedExceptions, uncheckedExplicit error returns, but ignoring one compilesResult<T, E>. Ignoring one is a warning, and clippy -D warnings makes it a build failure
Data racesSingle-threaded event loop; shared memory across workers is hand-managedThe GIL serialises bytecodePossible; found at runtime by the race detectorSend and Sync are checked at compile time. A data race in safe code does not build
Use after free, buffer overrunPrevented by the runtimePrevented by the runtimePrevented by the runtimePrevented by the compiler, and #![forbid(unsafe_code)] means a crate cannot opt out
What gets deployedThe runtime plus node_modulesThe interpreter plus site-packagesA static binaryOne compiled module
Where a mistake surfacesRuntimeRuntimeMostly runtimeCompile time

No benchmark appears here. We have not run one, and quoting someone else's would not make it ours. Each of these languages is a reasonable choice for a backend, and the ones with a garbage collector are easier to hire for; what Rust buys is that the composition rules on this page are enforced rather than documented.

Rules · Shipping

Stateless by construction.

Rust compiled to WebAssembly on Cloudflare Workers via workers-rs and axum. No static mut, no thread-local outliving a request. Request scope travels in axum extensions, and the conformance kit ships the concurrent-request test that proves it. Migrations are forward-only, SQL in a portable subset that runs on SQLite, D1 and Postgres, and CI runs every module's tests against both engines.

  • RFC 9457 application/problem+json errors with stable type URIs
  • HMAC-signed links with kid rotation and constant-time compare, so there is no session store
  • No account enumeration: identical 202 for every state
  • Rate limits on every public endpoint
  • Admin endpoints off entirely when the token is unset
  • CSV exports escape formula injection
  • Secrets redacted from logs; emails logged only as a truncated hash
  • PII minimalism: no IP or user-agent stored
  • One structured span per request, x-request-id on every response

Every snippet on this page is in the repository. Wrong Rust here would be fatal, so there is none invented.