/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

Four methods.

A module declares its name, the ports it requires, its migrations, and an axum router. Nothing else. 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.

crates/core/src/module.rs
pub trait Module: Send + Sync + 'static {
    fn name(&self) -> &'static str;          // mounted at /v1/<name>
    fn requires(&self) -> &'static [Port];   // build fails if one is missing
    fn migrations(&self) -> Migrations;      // include_str! SQL, portable subset
    fn router(&self, ctx: ModuleContext) -> axum::Router;
}

Ports

Ten trait objects. No vendor in sight.

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

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.

Request lifecycle · Shipping

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

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 today and Postgres when the adapter lands.

  • 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.