Expand description
cratefield-core is the runtime-agnostic kernel of the Factory Zero harness:
the Module contract, the Harness builder, port traits, RFC 9457
problem+json errors, the request Scope, an in-process EventBus and
a TemplateRegistry (ADR 0001, 0002, 0007).
Core depends only on http, axum (default features off), serde,
tracing, sea-query and pure-Rust crypto. It must never depend on
worker, wasm-bindgen, tokio, reqwest, sqlx or rusqlite, and
never touches std::fs or std::net — CI enforces this with a
cargo tree check and the wasm build of examples/venture.
cratefield.com · HARNESS · the open-source core
§The harness
Every product needs a backend, and almost none of them should be built from scratch. This is that backend, once.
A Rust harness you compile your own backend from: pick module crates, wire adapters, ship one stateless Worker with its own database. Cloudflare D1 today, a self-hosted native binary later, with no module rewrites in between.
This repository is the open-source core, MIT, and it is complete enough to run yourself today. Cratefield is the managed service being built on top of it: builds, migrations, secrets, domains and monitoring, so you do not have to operate any of it. That service is not built yet, and the site says so on every page.
Modules only see ports. A module never touches a Cloudflare binding, an environment variable, or a vendor client. It asks for a
Database, aMailer, aCaptcha. Adapters answer. That one rule is what makes the later move off Cloudflare a change of a single runtime crate.
Read docs/ARCHITECTURE.md for the full design. Decisions, including why the TypeScript attempt was thrown away, are in docs/adr. Security controls and reporting: docs/SECURITY.md. What we store and for how long: docs/PRIVACY.md. Which module version runs on which core: docs/COMPATIBILITY.md, generated and drift-checked in CI. How crates reach crates.io: docs/RELEASING.md.
§How a venture uses it
// src/harness.rs in a venture repo
Harness::builder()
.venture(Venture::new("acme", "acme.example.com")
.public_url("https://acme.example.com")
.cors_origins(["https://acme.example.com"]))
.module(EmailSignup::new().double_opt_in(true))
.module(Waitlist::new().products(["kontinuum", "undercover-rockstars"]))
.runtime(Cloudflare::new()
.db("DB")
.mailer(Resend::from_env())
.captcha(Turnstile::from_env()))
.build()?That is the whole composition. build() refuses a module that requires a port
the runtime does not provide, two modules claiming the same table or route, or
a module built against a different contract version. The venture template runs
it under cargo test, so a misconfiguration fails before wrangler deploy can.
§Shape
%%{init: {"theme":"base","themeVariables":{
"background":"transparent",
"fontFamily":"ui-monospace, SFMono-Regular, Menlo, monospace",
"fontSize":"13px",
"primaryColor":"#141416","primaryTextColor":"#EDEBE6","primaryBorderColor":"#3A3A3F",
"lineColor":"#6E6E76","textColor":"#8A8A8E",
"clusterBkg":"transparent","clusterBorder":"#3A3A3F",
"edgeLabelBackground":"#0E0E10"
}} }%%
flowchart LR
REQ(["HTTPS<br/>request"]):::req --> H
subgraph V["ONE VENTURE · ONE BINARY · ONE DATABASE"]
H["<b>Harness</b><br/>axum router<br/>/v1/<module>"]:::core
M1["email-signup"]:::mod
M2["waitlist"]:::mod
MX["your module"]:::ghost
P{{"<b>ports</b><br/>Database · Mailer<br/>Captcha · RateLimiter<br/>Signer · KeyValue"}}:::port
H --> M1 & M2 & MX --> P
end
subgraph A["ADAPTERS · THE ONLY VENDOR-AWARE CODE"]
DB[("D1")]:::vendor
KV[("KV")]:::vendor
RS["Resend"]:::vendor
TS["Turnstile"]:::vendor
PG[("Postgres<br/>native runtime")]:::vendor
end
P --> DB & KV & RS & TS
P -. "runtime-native" .-> PG
classDef req fill:#0E0E10,stroke:#4C6FFF,stroke-width:1.5px,color:#EDEBE6
classDef core fill:#141416,stroke:#4C6FFF,stroke-width:1.5px,color:#EDEBE6
classDef mod fill:#0E0E10,stroke:#3A3A3F,color:#EDEBE6
classDef ghost fill:transparent,stroke:#55555A,stroke-dasharray:4 3,color:#8A8A8E
classDef port fill:#141416,stroke:#EDEBE6,stroke-width:1.5px,color:#EDEBE6
classDef vendor fill:#0E0E10,stroke:#3A3A3F,color:#A9A8A5
classDef future fill:transparent,stroke:#55555A,stroke-dasharray:4 3,color:#8A8A8E§Crates
All public crates are cratefield-*, MIT, plus the cratefield facade that
pulls them together. Nothing is published to crates.io yet; depend on this
repository by git.
Most ventures want one line:
cratefield = { version = "0.1", features = ["cloudflare", "resend", "waitlist"] }The individual crates stay available and are the same types; the facade is a convenience, not a layer.
These crates were
factory0-*until the first release. Renaming a published crate breaks every consumer, so the rename had exactly one free moment: before anything reached crates.io. It was taken then (ADR 0011). Private modules stayfz-*and stay unpublished.
| Crate | Role |
|---|---|
cratefield | The facade: one dependency that re-exports the core and pulls in a runtime, adapters and modules by feature (ADR 0011, 0012). Start here |
cratefield-core | Module trait, Harness builder, port traits, problem+json errors, request scope, event bus, templates |
cratefield-runtime-cloudflare | workers-rs entry points; D1, KV, Rate Limiting and wait_until mapped to ports |
cratefield-adapter-resend | Mailer over the Resend REST API, with a NotConfigured mode until a sending domain is verified |
cratefield-adapter-turnstile | Captcha over Cloudflare Turnstile, fail-closed |
cratefield-adapter-sqlite | Database over rusqlite: every test, and single-node self-hosting |
cratefield-module-email-signup | Email signup with double opt-in, unsubscribe, admin export |
cratefield-module-waitlist | Per-product waitlist with confirm, position, referral codes |
cratefield-secrets | Envelope-encrypted secrets over the Database port, two tiers, ciphertexts bound to their row (#39) |
cratefield-kms | The KMS port: wrap and unwrap data keys, with a local-file provider that refuses production (ADR 0102) |
cratefield-ui | Renders the module surface as HTML at /ui: pages, fragments, in-process form dispatch, the cf-* styling contract (ADR 0010) |
cratefield-cli | Binary fz: migrations collect, doctor, modules |
cratefield-testing | Conformance kit every module, public or private, must pass |
cratefield-adapter-postgres | Database over sqlx for the native runtime (.github/workflows/parity.yml runs module suites against both SQLite and Postgres) |
cratefield-runtime-native | The same harness as a single binary on tokio: axum on a TCP listener, Redis RateLimiter, in-process cron |
cratefield-adapter-apns | Push over Apple Push Notification service, HTTP/2 through the HttpClient port — no vendor SDK |
cratefield-adapter-fcm | Push over Firebase Cloud Messaging (HTTP v1), the same shape as APNs |
cratefield-adapter-webpush | Push over Web Push (RFC 8030/8188/8291): browsers and UnifiedPush |
cratefield-push-auth | The provider tokens the push adapters present: ES256 for APNs and VAPID, RS256 for Google service accounts |
cratefield-push-wiring | Assembles the Push port from the environment: one env-variable table shared by serve(), fz push and fz doctor |
cratefield-adapter-stripe | Payments over the Stripe REST API |
cratefield-module-cms | A small content store with an editor: typed collections, versioned, in the venture’s own database |
cratefield-module-privacy | Subject access and erasure, assembled from what every other module declares it holds |
cratefield-module-notifications | Push, an in-app inbox and email from one notify(), with per-account per-category preferences (NOTIFICATIONS.md) |
cratefield-i18n | Server-side localisation: Fluent catalogs, BCP 47 negotiation, text direction |
cratefield-auth-client | Verifies auth tokens in a consuming app: JWKS fetch and cache, ES256, an axum extractor |
Everything else in the workspace is unpublished — publish = false is what
makes a crate private now, not a separate repository (ADR
0013):
| Crate | Role |
|---|---|
factory0-auth-* | The auth service: auth-core plus one crate per login method (passkeys, OIDC/Google/Apple, password, magic link, Meta) and the deployable auth-worker. cratefield-auth-client, which verifies its tokens in a consuming app, is published; the service itself is not |
fz-module-linkedin | Private Factory Zero module: run a LinkedIn Company Page from the harness |
cratefield-control-plane, cratefield-console, cratefield-accounts, cratefield-access, cratefield-catalog, cratefield-connections, cratefield-provisioning, cratefield-ui-generator | The managed service: sign up, pick modules, connect Cloudflare and SSO, get a running venture |
cratefield-introspect | Reads a database’s own catalog over the Database port (SQLite pragmas, Postgres information_schema) and answers in cratefield-tables’ vocabulary — the source the dashboard’s data screen renders |
Ventures live in ventures/: cratefield-waitlist serves
api.cratefield.com, and _template is the layout a new one copies.
§What a module is
A crate implementing one trait.
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;
// version, optional ports, tables, events, scheduled …
}A module is mounted one of two ways, and a caller cannot tell which. Compiled
in is the default this README describes: the crate is linked into the Worker.
Sidecar gives one module its own Worker, built and deployed separately and
mounted at the same /v1/<name> over a Cloudflare service binding, binding the
same database and secrets. It exists so a module whose source should not enter
the shared artifact can still run as a real module with real ports. Built:
examples/sidecar-module-template is the
Worker a customer deploys, host→sidecar event delivery crosses inside
wait_until (ADR 0017),
and CI proves a slow sidecar forward live
(wrangler dev sidecar event forward in .github/workflows/ci.yml, issue #258).
docs/MOUNTING.md is the runbook.
Migrations are plain SQL in a subset SQLite and Postgres both accept. Queries go through sea-query, which renders for either. Confirmation and unsubscribe links are HMAC-signed tokens with key rotation, so there is no session store. Request scope travels in axum extensions, never in shared state; the conformance kit includes the concurrent-request test that proves it.
§Roadmap
| Milestone | Contents | Issues |
|---|---|---|
| M0 Foundation | workspace tooling, core, Cloudflare runtime, Resend and Turnstile adapters, SQLite adapter, fz, testing kit | #1–#9 |
| M1 First modules | email-signup, waitlist, templates, security baseline, observability | #10–#14 |
| M2 First venture live | crates.io publishing, docs, contract versioning, api.factory0.ventures | #15–#17 |
| M3 Self-hosted portability ✅ | Postgres adapter (cratefield-adapter-postgres), native runtime (cratefield-runtime-native, examples/venture-native), parity suite (.github/workflows/parity.yml), data move (fz data export / fz data import) | #18–#21 |
One epic is still specified but not scheduled: #23
multi-tenant schema — the native runtime serves one venture per process today
(SECURITY.md). The other two former epics shipped:
#24 embedded secrets is
cratefield-secrets (envelope-encrypted over the Database port, docs/SECRETS-DESIGN.md),
and #56 custom modules is the
sidecar path (docs/MOUNTING.md).
Progress is visible in the milestones.
§Observability
One structured span per request carries request_id, method, route
(the matched path), module, status, duration_ms, ip_hash and
ua_family — never an email address. Workers Logs is enabled in the
template wrangler.toml ([observability] enabled = true); every
response also echoes x-request-id. To pull one request’s trail out of
the logs, filter on the id the API returned (needs a deployed Worker and
wrangler auth against your Cloudflare account, so CI cannot run it):
wrangler tail --format pretty --search <request-id>The error taxonomy (every problem slug, status and meaning) is
docs/ERRORS.md, generated from cratefield-core’s registry and checked
in CI for drift.
§Toolchain
Stable Rust pinned in rust-toolchain.toml, target wasm32-unknown-unknown,
worker-build, wrangler. CI runs
fmt, clippy -D warnings, test, cargo deny, and builds the example
venture to wasm so a native-only dependency cannot slip into a module.
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
(cd examples/venture && worker-build --release)§Layout
crates/
core/ cratefield-core
runtime-cloudflare/ cratefield-runtime-cloudflare
adapter-resend/ cratefield-adapter-resend
adapter-turnstile/ cratefield-adapter-turnstile
adapter-sqlite/ cratefield-adapter-sqlite
module-email-signup/ cratefield-module-email-signup
module-waitlist/ cratefield-module-waitlist
kms/ cratefield-kms
secrets/ cratefield-secrets
ui/ cratefield-ui
cli/ cratefield-cli → fz
testing/ cratefield-testing
examples/
venture/ smallest complete venture; CI builds it to wasm
docs/
ARCHITECTURE.md
KEY-ROTATION.md rotating data keys and re-wrapping under a new master key
MIGRATION-STREAMS.md two repositories applying migrations to one database
RECONCILIATION.md boot-time reconciliation across tenant databases
MOUNTING.md compile a module in, or run it as a sidecar
UI.md the UI surface, its markup contract, UiSpec, admin
ui-llms.txt the same contract written for a generator
adr/ 0000 … 0010
tools/
banner-render.html source of the README banner
render-banner.sh regenerates it with headless Chrome§License
MIT. Built in the open for Cratefield, a Factory Zero venture.
Structs§
- Action
- One route the module serves, described for a renderer.
- Blob
Object - A stored object: its bytes and the content type to serve it with.
- Bounded
Http Client - Wraps any
HttpClientso the port’s bounds hold on every runtime: the deadline is enforced through theClockport (the only timer core is allowed to use), and the response is refused — by declared length before anything else, then by actual size — when it exceeds theHttpPolicycap. - Brand
- Venture branding for mail templates (issue #12). Defaults are text-only: factory-zero orange accent, no logo, no footer line.
- Captcha
Binding - What a captcha adapter is actually bound to (issue #133). The harness
reads this at build time to decide whether a production venture’s
HumanFormroutes can really be verified — providing the port is not the same as configuring it. - Catalog
Entry - One declaration, and the module that owns the table.
- Charge
- A created charge/payment-intent and its status as Stripe reported it.
- Checkout
Request - A one-time hosted checkout (Stripe Checkout in
paymentmode). - Checkout
Session - The hosted page to send the browser to, and the session id to reconcile on.
- Column
- A column of a
View::Table. - Config
Error - Accumulates every configuration problem so
Harness::buildcan report them together instead of one at a time. - Connect
Account Link - The Connect account id (persist it) and the hosted onboarding URL.
- Connect
Account Link Request - Onboards a Connect account (a coach) and returns a hosted onboarding link.
- Decision
- Empty
Config - A configuration that always returns
None(tests, offline builds). - Event
Bus - Registry of handlers, built once by
Harness::buildfrom every module’sevents(). Cheap to clone (twoArcs). - Form
- A form (
application/x-www-form-urlencoded) extractor whose rejections are problem+json with the same shape asJson’s: body reads fail through the shared 413 slug (the size limit), everything else is a 400 validation problem. Needed for cross-siteform_postcallbacks (issue #46). - Harness
- A built harness: immutable after
build(). - Harness
Builder - Builder:
.venture(..),.module(..),.runtime(..),.template(..), then.build(). - Harness
Config - The harness-level keys, parsed once from the environment
Config(issue #3):HARNESS_SECRET(required, ≥ 32 bytes),HARNESS_SECRET_PREVIOUS(optional),ADMIN_TOKEN(optional),ENV(development|staging|production, defaultdevelopment), and the issue #137 key-ring keys:HARNESS_SECRET_REVOKED(optional, comma-separated key ids whose tokens must be refused immediately) andHARNESS_VENTURE(optional label binding tokens to this venture, so one venture’s links cannot verify in another that shares its secret by mistake). - Hmac
Signer - HMAC-SHA256 signer over a
KeyRing(ADR 0006, ADR 0014). - Http
Policy - Per-operation outbound bounds, attached to a request as an extension
and resolved by every implementation with
HttpPolicy::of_request. Both fields clamp to the port maxima: a caller may only tighten. - Implicit
Tenant - The resolver a deployment with no control database uses: every
host is the one venture, always
active. - Inbox
- A dedup ledger over the
Databaseport. Construct it with the table the owning module declares (e.g."<module>_inbox"). - Json
- A
Jsonextractor and response whose rejections and serializations are problem+json (architecture section 6). Deserialization failures become a400 validation-failedproblem listing the field. - KeyRing
- A bounded set of token-signing keys with explicit states (issue #137).
- Line
Item - One line on a checkout: a name shown to the buyer and its price.
- LocKeys
- Native localisation keys. The strings are looked up in the app’s own catalogue by the operating system, so a notification can be localised without the server knowing the device’s language. Reserved here and mapped by the adapters; rendering is the i18n child’s job.
- MapConfig
- A
Configbacked by a map (tests,fz doctorwith process env). - Member
- A connected participant, identified by the venture after it verified the
upgrade request’s bearer token (through the
Signeror the auth client). ARoomHandleronly ever sees this verified id, never a raw token. - Message
- An outbound mail.
textis always sent alongsidehtml. - Migrations
- The module’s migrations, per dialect.
postgresdiffers fromsqliteonly where the SQL truly differs (ADR 0004). - Module
Config - Typed view over a
Configfor one module: prefixes every key with the module name inSCREAMING_SNAKEand parses values with defaults (issue #3). - Module
Context - Everything a module’s router needs: its declared ports, the typed config, the bus, the templates and the venture identity.
- Module
Surface - One module’s entry in the composed document.
- Money
- An amount in a currency’s minor units (cents), the way Stripe takes and
reports money.
currencyis a lowercase ISO-4217 code ("usd"). - Noop
Defer - Drops deferred futures with a warning. Used when no runtime defer is available; tests that assert on deferred work supply their own.
- Notification
- One notification, in transport-neutral terms.
datais the custom key-value payload the app reads;collapse_idcoalesces notifications the user has not seen yet. - Outbox
- A durable work queue over the
Databaseport. Construct it with the table the owning module declares (e.g."<module>_outbox"). - Outbox
Record - A leased outbox record handed to a drainer.
- Payload
- The signed payload:
{ purpose, subject, exp?, kid }. - Personal
Data Catalog - Every personal-data declaration in a composition, with the module that made it.
- Personal
Data Set - One table’s worth of personal data, declared by the module that owns it.
- Ports
- The per-request bundle of resolved port implementations plus the typed config the runtime built from environment/secrets.
- Problem
- An API error, serialized as
application/problem+json. - Problem
Def - Definition of one problem slug.
- Redacting
Visitor - A
tracingfield visitor that records(name, redacted value)pairs into a map. Runtimes use it in their formatters; tests use it to prove the redaction rules. - Refund
- A created refund.
- Refund
Request - A refund of a prior payment: the whole amount when
amountisNone, else a partial refund. - Rendered
- A rendered mail, ready for
Message. - Rendered
Surface - A document serialized once, with the strong
ETagclients revalidate against. Built atHarness::buildfor both the public and the admin variant. - RingKey
- One ring entry. Deliberately not
Debug: the ring’s debug view (KeyRing::states) shows ids and states but never key material (issue #135’s scrubbing rule applies to key bytes too). - Routing
Push - Dispatches by
Recipientvariant to the adapter a venture configured for that transport, so venture code holds oneArc<dyn Push>and never matches on the transport itself (ADR 0015). - Row
- One result row: ordered
(column name, value)pairs. - Rows
- A small owned row model. No engine types leak past this point.
- Scope
- Per-request scope, inserted into extensions by the request-id layer
(issue #2). Handlers receive it through the
Scopeextractor; there is no ambient “current request”. - Scoped
Blob - Wraps a
Blobso every key is prefixed with<module>/and no key can escape it. The harness applies this inPorts::view_for, so a module sees a store scoped to itself — the blob equivalent of the table-ownership check. - Send
Cooldown - A cooldown ledger over the
Databaseport. Construct it with the table the owning module declares (e.g."<module>_send_cooldown"); the owning module shipscreate_table_sqlas a migration. - Sidecar
Mount - One mounted sidecar.
- Sidecar
Mounts - The mount table, parsed from configuration.
- SqlMigration
- One migration step, embedded with
include_str!fromcrates/<module>/migrations/<dialect>/NNNN_name.sql(issue #8). - Statement
- A rendered SQL statement:
(sql, values)with?placeholders, produced by rendering a sea-query query for a dialect. Modules build queries with sea-query and render through the helpers on this type (or let adapters do it); adapters bindvaluespositionally. - Subject
Via - One hop to the subject, through a table that holds the id requests are made with.
- Subscription
Checkout Request - A recurring hosted checkout (Stripe Checkout in
subscriptionmode) against a Stripe Price the venture configured (e.g.$15/mowith a trial). - Surface
- What a module declares from
Module::surface. - Surface
Document - The document
GET /__surfaceserves: composed atHarness::build, one entry per module in mount order, modules with an empty surface omitted. - System
Clock - Real wall clock (
timecrate). Its timeout runs futures to completion — tests that need a real timeout supply their own clock. - Template
Registry - Immutable registry: venture overrides are inserted after module defaults
at
Harness::build, so they win on id collision. - Tenant
- The tenant a request belongs to. Immutable, and built only by core’s resolution layer from a registry row — never from a header, a path segment, or module code.
- Tenant
Conn - A database handle bound to the request’s tenant.
- Tenant
Id - A tenant’s key in the control database’s registry.
- Token
Policy - A per-purpose lifetime ceiling (ADR 0014, issue #137).
- Transfer
Charge - A destination charge with an application fee: the buyer is charged
amount,application_feeis kept by the platform, and the remainder is transferred todestination_account(the coach’s Connect account). - UiContext
- What a UI renderer gets from the harness (ADR 0010). Built by
Harness::routerfor every router it assembles. - Ulid
IdGen - Real ULID generator (monotonic per process).
- Venture
- Identity and CORS configuration for the venture this harness serves.
- Venture
Surface - The venture identity a renderer needs.
- Verdict
- A captcha verification verdict.
ok: falsewith areasonfrom the provider’serror-codes; transport-level unavailability surfaces asok: false, reason: "unavailable"unless the adapter is configured fail-open (staging only). - Webhook
Event - A webhook event the adapter has verified (signature + timestamp) before
returning.
kindis Stripe’s event type ("checkout.session.completed");datais the event’sdata.objectfor the module to interpret. - Write
Guards - What the composed modules demand of the runtime, module by module.
Enums§
- Audience
- Who an action is for. Drives the public/admin split of
/__surfaceand, in the renderer, which pages need the admin session. - Blob
Error - Blob store failures.
- Captcha
Error - Data
Kind - The category a row falls into, in the vocabulary a privacy page and an App Store questionnaire both use.
- DbError
- Database failures, sanitized for logs and problem details.
- Dispatch
Error - Disposition
- What erasure does to these rows.
- Http
Error - KeyState
- A key’s role in the ring (ADR 0014, issue #137).
- Kid
- Which key in the
crate::KeyRinga token was signed with. Tokens name their key so rotation never breaks links in flight. - KvError
- Mail
Error - Mailer failures, mapped by the adapter from provider responses.
- Origin
Error - Why a string is not an origin this harness will name.
- Outcome
- What the browser should do with a successful response.
- Payments
Error - Payment failures.
NotConfiguredlets a venture build and run without Stripe (the port reports it rather than erroring); the rest map an upstream failure.SignatureInvalidis separated so a webhook handler answers400and never processes an unverified event. - Platform
- The transport a
Recipientis reached over, which is not a statement about the device: a UnifiedPush endpoint isPlatform::Webon an Android phone (ADR 0015). - Port
- Every port a module can declare in
requires()/optional()(architecture section 4). - Priority
- How urgently the notification should be delivered. Maps to APNs priority
10(deliver now, may wake the device) and5(deliver to save power); toandroid.priorityHIGH/NORMAL; to the Web PushUrgencyheaderhigh/normal. - Push
Error - Push failures.
Unregisteredis separated because the caller must act on it — the recipient is dead and should be pruned — where the others are transient or a bad request. - Push
Outcome - The result of a send that the provider accepted.
- Rate
Limit - Outcome of consulting the limiter for one request.
- Rate
Limit Error - Rate
Limit Failure - What a limiter transport error means for the request (issue #133). Every call site picks one deliberately; it used to be an implicit “allow” buried in each module’s copy of the limiter loop.
- Realtime
Error - Realtime failures.
- Recipient
- Where a notification is sent. One variant per push transport, because the three do not share a shape: APNs and FCM take an opaque token, Web Push takes a subscription of three parts (RFC 8291).
- Resolution
- What a deployment’s registry says about a host.
- Route
Policy - How a request to a declared route proves it is legitimate.
- Send
Outcome - Result of a send attempt.
- Signature
Error - Failures surfaced by
verifybeyond “the token is simply invalid”, which is reported asNone. - Signer
Error - Errors from constructing an
HmacSigneror aKeyRing. - Template
Error - Tenant
DbError - Why a tenant’s database could not be handed over. Deliberately carries the tenant id and nothing else — no DSN, no driver message.
- Tenant
Status - A tenant database’s reconciliation status.
- Venture
Env - Deployment environment. Mirrors the
ENVconfig key;Productiondrives the mandatory-captcha rule (architecture section 11). - View
- How actions compose into something to render.
Constants§
- ALLOW_
UNPROTECTED_ WRITES - Config key holding an operator’s explicit, recorded acceptance that this deployment serves guarded routes it cannot fully protect (issue #143).
- CONFIRM_
TOKEN_ MAX_ TTL_ SECS - Seven days: the ADR 0006 confirmation default, expressed as a policy ceiling rather than a caller habit.
- DEFAULT_
RESPONSE_ TIMEOUT - Deadline applied to an outbound call that asks for none. Longer than the strictest in-tree self-bound (Turnstile’s 5s verify), shorter than anything that could pin a request.
- DEFAULT_
TOKEN_ MAX_ TTL_ SECS - The default ceiling for any purpose not listed explicitly. An unknown purpose can never mint a non-expiring token by omission.
- FORMULA_
PREFIXES - Characters that make a cell a formula when it starts with one of them.
- GATEWAY_
ADMIN_ PURPOSE - The purpose the host mints only after its own
require_adminhas passed for an admin path (issue #131). It is what lets a sidecar re-materialize its own admin token: a token proving nothing more than “this came through the host” must not open the admin plane, or any captured forwarded stamp would be an admin credential for its whole lifetime. - GATEWAY_
PURPOSE - The token purpose bound into the gateway MAC. A token minted for any other purpose — a confirm link, an admin session — cannot pass verification, and vice versa (issue #137’s rule, applied to #131).
- GATEWAY_
TOKEN_ TTL_ SECS - Gateway token lifetime: a forwarded request completes in one hop, so a minute is generous. Short TTLs shrink the replay window a stolen token buys an attacker (issue #131).
- HARNESS_
API - Contract version shared by core and every module.
Harness::buildrejects modules whoseharness_apidiffers. Bumped only on breaking contract changes;cratefield-core’s major follows it. - HARNESS_
ONE_ WORKER - Truthy declares “this venture deploys as one Worker”: mounting a sidecar
then contradicts the shape, and
SidecarMounts::from_configrejects the mount table rather than starting a deployment that cannot reach it (issue #131). - HARNESS_
SIDECARS - Config key holding the mount table, a JSON object of
{"<module name>": "<service binding>"}. - HINT_
KEYWORDS - The
x-cf-*extension keywords the renderer understands on a field schema. Anything else underx-cf-is ignored, never an error, so a module can target a newer renderer than the one that serves it. - IMPLICIT_
TENANT - The registry id a deployment without a registry resolves to.
- MAX_
BLOB_ BYTES - Hard ceiling on one stored object (issue #136). The port exists for
small media — a coach’s voice clip, an avatar — and every caller
reaches it through
ScopedBlobor an adapter that enforces this bound before allocating or writing. Ten MiB is well past any legit clip at conversational bitrates and far below what threatens a self-hosted process or an isolate that buffers the bytes. - MAX_
BODY_ BYTES - Default request body limit for
/v1/*JSON endpoints. - MAX_
CONCURRENT_ REQUESTS - Per-process budget for concurrent outbound requests. The native runtime shares one connection pool across the tenant it serves; this is what stops one request’s fan-out (say a cron fanning out emails) from monopolising the pool and the response-buffer memory behind it.
- MAX_
EMAIL_ BYTES - The maximum accepted address length in bytes (RFC 5321 “forward-path”).
- MAX_
EXPORT_ ROWS - Maximum rows one admin-export page may carry (issue #136). Exports are
paged (
limit/offsetquery params) and clamped to this: an unboundedSELECT *of a subscriber table into one in-memory String is an outage a hostile sign-up wave can trigger, and the bound is what a Worker isolate budgets for one response.x-cf-export-more: truetells the operator a next page exists. - MAX_
KID_ NAME - The longest accepted
Kid::Namedwire name. Key ids ride inside every token; the bound keeps a hostile payload from claiming arbitrary storage. - MAX_
LOCAL_ BYTES - The maximum local-part length in bytes (RFC 5321).
- MAX_
RESPONSE_ BYTES - Hard ceiling on a response body this port may return: an adapter’s
largest legitimate upstream reply (JWKS documents, Stripe objects,
LinkedIn
peoplepayloads) is orders of magnitude below it, and it is far below anything that threatens isolate memory. Callers may lower it per operation, never raise it. - MAX_
RESPONSE_ TIMEOUT - Ceiling on a caller-supplied deadline. A 30 s Worker request cannot responsibly spend more than half of it waiting on one upstream.
- MAX_
SIDECAR_ ACTIONS - Largest action list a merged sidecar surface may declare (issue #131).
- MAX_
SIDECAR_ SURFACE_ BYTES - Largest sidecar surface document the host will merge (issue #131). A whole module’s declaration is a few kilobytes; anything approaching this cap is hostile or accidental, and parsing it costs the host.
- MAX_
SIDECAR_ VIEWS - Largest view list a merged sidecar surface may declare (issue #131).
- MIN_
SECRET_ BYTES - Minimum secret length.
HARNESS_SECRETmust be at least 32 bytes. - SIDECAR_
GATEWAY_ SECRET - Shared secret between the host and a sidecar that enforces the gateway
(issue #131). The host mints short-lived per-request tokens with it; the
sidecar verifies them on
/v1/*and/__surface. It is its own secret —HARNESS_SECRETstays venture-local (ADR 0009) — and must appear on both ends of a mounted pair. - SIDECAR_
REQUIRE_ GATEWAY - Sidecar-side switch: truthy means every
/v1/*and/__surfacerequest must carry a valid gateway token minted withSIDECAR_GATEWAY_SECRET. Requiring it without the secret is a configuration error and fails closed, not open (issue #131). - SLUGS
- STATUS_
TOKEN_ MAX_ TTL_ SECS - The status-token policy (issue #137): a waitlist position is not secret, but a URL that never expires is a URL forever in logs, forwards and bookmarks. Ninety days, renewed on every confirm hit.
- SURFACE_
API - Contract version of the surface document, independent of
HARNESS_API: a renderer or the control plane checks it before reading the document. - UNSUBSCRIBE_
ACTION - The one action with a deliberately non-expiring ceiling, justified in
ADR 0014: unsubscribe must always be reachable, and the longevity the
signed link used to buy from the global key ring is now also carried
by the per-subscription opaque token in
module-email-signup, which outlives any key rotation because it lives in the subscriber’s row. - X_
HARNESS_ API - Contract version stamped on every harness response, checked by the host on every forwarded response. Stamping beats a cold-start handshake because an isolate outlives a sidecar redeploy (ADR 0009).
- X_
HARNESS_ GATEWAY - Gateway token header, minted by the host per forwarded request and
verified by a sidecar that set
SIDECAR_REQUIRE_GATEWAY(issue #131). - X_
HARNESS_ MODULE - Module name stamped alongside
X_HARNESS_API. - X_
REQUEST_ ID x-request-id: accepted from the client when it matches^[A-Za-z0-9_-]{8,128}$, otherwise generated as a ULID. Always set on the response (architecture section 6).
Traits§
- Blob
- A blob store. Keys are module-prefixed; the harness wraps this in a
ScopedBlobper module so a module cannot name another’s objects. - Captcha
- Clock
- Config
- Read-only key/value configuration, resolved per runtime from environment
variables and secrets (Workers
Env) or the process environment. - Database
- Execute statements against the venture database. Implementations: D1 (Workers), rusqlite (tests, self-hosted), Postgres (phase 3).
- Defer
- Dispatcher
- Http
Client - IdGen
- KeyValue
- Mailer
- Module
- A Factory Zero module. Object-safe; composed as
Arc<dyn Module>. - Payments
- Moves money for a venture. Stripe today; the trait names only Stripe identifiers and hosted URLs, never card data.
- Push
- Sends notifications to a device, over whichever transport the
Recipientnames. An adapter serves one transport and returnsPushError::unsupported_recipientfor the rest;RoutingPushfans a mixed set of recipients out across the adapters a venture configured. - Rate
Limiter - Realtime
- Poke a room from outside a socket — a module’s HTTP handler (e.g. an admin
ends a session) or a cron. The socket lifecycle itself is the runtime’s job
(it calls
RoomHandler); this is the part a module holds viaPorts. - Resolve
Tenant - How a deployment turns a request’s host into a tenant.
- Room
Context - The operations a
RoomHandlerperforms on its room while handling an event. The handler holds no sockets of its own — it reaches them through this context — so a Durable Object can hibernate between events. - Room
Handler - The protocol for one kind of room. A module implements it; the runtime owns the sockets and calls these as events arrive.
- Runtime
- A runtime resolves environment bindings into
Portsand declares statically which ports it can provide, soHarness::buildcan reject a module that requires something the runtime will never hand it (ADR 0002). Reference implementation:cratefield-runtime-cloudflare. - Signer
- Produces and verifies
base64url(json).base64url(mac)tokens where the MAC is computed over the encoded payload string, so a token has exactly one valid encoding (ADR 0006). - Surface
Source - Where the current surface comes from (issue #76). With no sidecar
mounted this is the document composed at build; with sidecars, each
call fetches every mounted sidecar’s
/__surface(public part) and merges it in, so a sidecar redeploy is seen on the next request (ADR 0009). An unreachable sidecar contributes nothing and is logged. - Template
- One template.
datais the caller’s JSON payload;localeis the requested locale tag (en,de, …) used by the implementation for its own variants. - Tenant
Databases - How a deployment turns a resolved tenant into a database handle.
- Tenant
Routing - A deployment’s tenant plane: a host becomes a tenant, and a tenant becomes a database handle.
- TryFrom
Value - Conversion from a sea-query
SeaValuefor typed row access. - UiMount
- A renderer the venture mounts at
/uiwithHarnessBuilder::ui(ADR 0010). Core defines the seam;cratefield-uiis the implementation, kept out of core so a venture without a UI carries nomaud.
Functions§
- assert_
migration_ set - Rejects a malformed migration set at compile time.
- bearer_
token - Extracts the bearer token from
Authorization: Bearer <token>. - captcha_
effective - Whether the runtime’s
Captchaport can really verify a token in production: the port is provided and, when the adapter reports, hostname-bound and not fail-open. An adapter that does not report (Captcha::binding=None) counts as effective — presence is all the harness can know about a third-party implementation; the adapter contract carries the duty to fail closed per request. - card_
data_ hit - The first card-data fragment
textcontains (case-insensitive), orNone. Keeps card numbers, verification codes and full expiry out of migrations and secret names — with a normal Stripe integration none of them should exist (issue #44). - check_
blob_ size - Rejects a write whose payload exceeds
MAX_BLOB_BYTES(issue #136). EveryBlob::putimplementation MUST call this before touching storage;ScopedBlob(the module-facing wrapper) already does, so an adapter’s own call is the second, defence-in-depth gate for runtimes-wired stores and direct users. - check_
rate_ limit - Shared limiter loop: every key must pass, and a transport error is
resolved by
on_failureat the call site rather than per-module convention. No limiter configured (None) allows the request: composition chose to run without one. - client_
ip - The client IP for rate limiting, from the headers.
- constant_
time_ eq - Constant-time equality of two secrets, via fixed-length digests so timing does not leak the configured token’s length.
- created_
tables - Every table
sqlleaves behind, in the order it creates them. - csv_
escape - Escapes one CSV field: formula-guard, then RFC 4180 quoting.
- csv_row
- Escapes and joins one CSV row, with a trailing newline.
- declared_
content_ length - The declared
Content-Lengthof a response, when it parses as a non-negative size. Implementations check this before touching the body, so an oversized declared length is refused without allocating for it (issue #136). - deployed_
env - The environment this deployment runs in (issue #143).
- env_
disagreement - Whether the deployment’s environment contradicts the compiled one (issue #143). Not an error by itself — the stricter answer wins — but it means the build-time gate ran against the wrong environment, so the caller re-checks and says so.
- harness_
api_ mismatch - The message
Harness::buildandfz doctorreport for a module whoseModule::harness_apidiffers from core’s: it names the module, the module crate’s version, the API it targets, and thecratefield-corecrate with its version and API (issue #17). - hint_
field - Sets one
x-cf-*(or any) keyword on a field of an object schema after derivation, for hints that only exist at runtime: aselectwhose options are the configured product list. Unknown fields are ignored so a rename in the body type cannot panic at build. - invalid_
email_ problem - A
400 validation-failedproblem for a rejected address. - is_
email_ field - Whether a field is expected to carry an email address.
- is_
idempotent_ sql - True when the SQL carries an idempotence guard, so re-running it
after a crash is safe. Required of every migration marked
transactional: false(RECONCILIATION.md §4). - is_
plain_ identifier - An unquoted SQL identifier: ASCII letters, digits and underscore, starting with a letter or underscore, at most 63 bytes — the shortest limit among the dialects the harness targets (Postgres truncates there; SQLite does not care).
- is_
secret_ field - Whether a field name marks a secret: matches
(?i)secret|token|key|authorization|password. - is_
valid - A conservative validator: non-empty, one
@, sane lengths, an ASCII local part from the unreserved set, and a dot-separated alphanumeric domain with no empty or hyphen-edge labels. - lint_
card_ data - Card-data column or table names found in
sql, as(fragment, why)pairs. Comments and string literals are ignored, so documenting the rule does not trip it. - lint_
portable_ sql - Returns
(token, explanation)pairs found insql. - migration_
checksum - The sha256 of a migration’s SQL, lowercase hex. Recorded in
harness_migrationswhen the migration is applied, so a later run can tell “already applied” from “applied, then edited” — the rule forward-only migrations rest on, enforced by the database rather than by a lockfile in one repository (issues #28, #34). - migration_
edited - The message a migration whose recorded checksum no longer matches gets. Shared so both engines say the same thing.
- migration_
missing_ guard - The message a non-transactional migration without an idempotence guard gets. Shared so both engines say the same thing.
- migration_
tables - Every table a module’s migrations leave behind, on either dialect.
- normalize_
email - Trim, Unicode NFC normalise, then lowercase. Idempotent.
- origin_
of - The RFC 6454 origin of
url: scheme, host and non-default port, ASCII serialized. - payments_
effective - Whether the runtime can verify webhook signatures for
RoutePolicy::Signature-guarded routes. Presence of thePaymentsbar: signature verification is the adapter’s per-request duty (verify_webhook+Inbox), and which secret it checks against is deploy configuration thatfz doctorreads from the environment. - problem_
registry - Every core slug definition, for tests and docs.
- production_
readiness - The production abuse-control gate (issue #133), as collected error
strings (the
ConfigErrorconvention: report every problem at once). Empty for anything butProduction; passallow_no_captcha = Some(reason)(thefz doctoroverride) to downgrade the CAPTCHA refusal to nothing — the runtime path has no override and always fails closed. - rate_
limit_ keys - The rate-limit keys for one request: always
ip:<ip>(orip:unknownwhen no address is visible), plusemail:<normalized>when an address is known. The limiter is consulted per key, in order. - rate_
limited - A
429 rate-limitedproblem carryingRetry-After: <seconds>when the limiter reported a pause (architecture section 6). - redacted_
value - How one recorded field is stored:
[redacted]for secrets, the hash for emails, the value passed throughscrub_textotherwise (issue #135: a secret inside a genericuri/message/errorvalue is invisible to the field-name rules alone). - request_
id_ is_ valid - The character class and length bounds of an accepted request id.
- require_
admin - Checks
Authorization: Bearer <ADMIN_TOKEN>for one admin request. - retry_
after Retry-After(RFC 9110 §10.2.3) as a duration, so a caller can honour what a provider asked for.- schema_
for - Generates the schema for
Tthe way every action does: draft 2020-12, definitions inlined so a renderer never has to resolve$ref. - scrub_
request_ url - Cuts one known request URL back to its origin everywhere it appears
in
message, and redacts that request’s bare target as well (issues #228, #229). - scrub_
text - Scrubs secrets out of a free-text value (issue #135).
- set_
error_ forwarder - Installs a process-wide forwarder for internal-error diagnostics (architecture section 11).
- set_
log_ pseudonym_ key - Installs the key that
subject_hashuses to pseudonymise email values in logs (issue #135). A bare SHA-256 of a low-entropy email is dictionary-reversible, so a runtime derives this fromHARNESS_SECRETand installs it once at startup;subject_hashthen emitsHMAC(key, email)instead. Unset (tests, or a runtime whoseHARNESS_SECRETfailed validation) makessubject_hashemit a fixed all-zero000000000000placeholder — fail-closed; no digest of the address ever reaches a sink. Boot-time infrastructure, not request state (ADR 0007); first-install-wins. - signer_
effective - Whether the runtime can issue and verify the artifacts a
RoutePolicy::SignedLinkroute rests on (issue #143). - subject_
hash - The redacted form of an email-ish value: a 12-hex pseudonym, no
@ever reaching the logs. When a pseudonym key is installed it isHMAC-SHA256(key, domain ‖ value)(a keyed pseudonym, resistant to dictionary reversal); when it is not, a fixed all-zero placeholder — correlation is lost, but a bare digest never leaks (issue #135, fail-closed). - timeout
- Typed wrapper over
Clock::timeout_any.Nonemeans the clock abandoned the future afterafter. - ttl_
secs - A
Notification::ttlas the whole seconds every push protocol takes. - undeclared_
tables - The tables a module owns and has said nothing about: the gap this type exists to close, named so a test can fail on it.
- unlisted_
tables - The tables a module’s own migrations create and its
Module::tablesnever mentions: the hole the declaration rule cannot see (issue #272). - unprotected_
writes_ override - The operator’s recorded reason for serving guarded routes unprotected, if they set one (issue #143).
- validation_
error - The reason an address is rejected, for problem
details. - verify_
human_ form - The shared CAPTCHA gate for
HumanFormhandlers (issue #133): the single place a module asks “is this form submission a human?”.
Type Aliases§
- AnyError
- Error type for handler and scheduled-work results.
- BoxFuture
- An owned dynamically typed
Futurefor use in cases where you can’t statically type your result or need to add some indirection. - Event
Handler - A registered handler: receives the emitting request’s scope and the payload.
- Event
Name - Event names are
"<module>.<event>", e.g.waitlist.confirmed.