Skip to main content

Crate cratefield_core

Crate cratefield_core 

Source
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 Harness. The open-source core. Modules are crates, compiled into one stateless Worker with its own database.

Status: M2 in progress Language: Rust Target: wasm32 on Cloudflare Workers Router: axum Database: D1 now, Postgres later License: MIT

parity: module suites on SQLite and Postgres (matrix)

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, a Mailer, a Captcha. 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/&lt;module&gt;"]:::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 stay fz-* and stay unpublished.

CrateRole
cratefieldThe facade: one dependency that re-exports the core and pulls in a runtime, adapters and modules by feature (ADR 0011, 0012). Start here
cratefield-coreModule trait, Harness builder, port traits, problem+json errors, request scope, event bus, templates
cratefield-runtime-cloudflareworkers-rs entry points; D1, KV, Rate Limiting and wait_until mapped to ports
cratefield-adapter-resendMailer over the Resend REST API, with a NotConfigured mode until a sending domain is verified
cratefield-adapter-turnstileCaptcha over Cloudflare Turnstile, fail-closed
cratefield-adapter-sqliteDatabase over rusqlite: every test, and single-node self-hosting
cratefield-module-email-signupEmail signup with double opt-in, unsubscribe, admin export
cratefield-module-waitlistPer-product waitlist with confirm, position, referral codes
cratefield-secretsEnvelope-encrypted secrets over the Database port, two tiers, ciphertexts bound to their row (#39)
cratefield-kmsThe KMS port: wrap and unwrap data keys, with a local-file provider that refuses production (ADR 0102)
cratefield-uiRenders the module surface as HTML at /ui: pages, fragments, in-process form dispatch, the cf-* styling contract (ADR 0010)
cratefield-cliBinary fz: migrations collect, doctor, modules
cratefield-testingConformance kit every module, public or private, must pass
cratefield-adapter-postgresDatabase over sqlx for the native runtime (.github/workflows/parity.yml runs module suites against both SQLite and Postgres)
cratefield-runtime-nativeThe same harness as a single binary on tokio: axum on a TCP listener, Redis RateLimiter, in-process cron
cratefield-adapter-apnsPush over Apple Push Notification service, HTTP/2 through the HttpClient port — no vendor SDK
cratefield-adapter-fcmPush over Firebase Cloud Messaging (HTTP v1), the same shape as APNs
cratefield-adapter-webpushPush over Web Push (RFC 8030/8188/8291): browsers and UnifiedPush
cratefield-push-authThe provider tokens the push adapters present: ES256 for APNs and VAPID, RS256 for Google service accounts
cratefield-push-wiringAssembles the Push port from the environment: one env-variable table shared by serve(), fz push and fz doctor
cratefield-adapter-stripePayments over the Stripe REST API
cratefield-module-cmsA small content store with an editor: typed collections, versioned, in the venture’s own database
cratefield-module-privacySubject access and erasure, assembled from what every other module declares it holds
cratefield-module-notificationsPush, an in-app inbox and email from one notify(), with per-account per-category preferences (NOTIFICATIONS.md)
cratefield-i18nServer-side localisation: Fluent catalogs, BCP 47 negotiation, text direction
cratefield-auth-clientVerifies 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):

CrateRole
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-linkedinPrivate 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-generatorThe managed service: sign up, pick modules, connect Cloudflare and SSO, get a running venture
cratefield-introspectReads 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

MilestoneContentsIssues
M0 Foundationworkspace tooling, core, Cloudflare runtime, Resend and Turnstile adapters, SQLite adapter, fz, testing kit#1–#9
M1 First modulesemail-signup, waitlist, templates, security baseline, observability#10–#14
M2 First venture livecrates.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.
BlobObject
A stored object: its bytes and the content type to serve it with.
BoundedHttpClient
Wraps any HttpClient so the port’s bounds hold on every runtime: the deadline is enforced through the Clock port (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 the HttpPolicy cap.
Brand
Venture branding for mail templates (issue #12). Defaults are text-only: factory-zero orange accent, no logo, no footer line.
CaptchaBinding
What a captcha adapter is actually bound to (issue #133). The harness reads this at build time to decide whether a production venture’s HumanForm routes can really be verified — providing the port is not the same as configuring it.
CatalogEntry
One declaration, and the module that owns the table.
Charge
A created charge/payment-intent and its status as Stripe reported it.
CheckoutRequest
A one-time hosted checkout (Stripe Checkout in payment mode).
CheckoutSession
The hosted page to send the browser to, and the session id to reconcile on.
Column
A column of a View::Table.
ConfigError
Accumulates every configuration problem so Harness::build can report them together instead of one at a time.
ConnectAccountLink
The Connect account id (persist it) and the hosted onboarding URL.
ConnectAccountLinkRequest
Onboards a Connect account (a coach) and returns a hosted onboarding link.
Decision
EmptyConfig
A configuration that always returns None (tests, offline builds).
EventBus
Registry of handlers, built once by Harness::build from every module’s events(). Cheap to clone (two Arcs).
Form
A form (application/x-www-form-urlencoded) extractor whose rejections are problem+json with the same shape as Json’s: body reads fail through the shared 413 slug (the size limit), everything else is a 400 validation problem. Needed for cross-site form_post callbacks (issue #46).
Harness
A built harness: immutable after build().
HarnessBuilder
Builder: .venture(..), .module(..), .runtime(..), .template(..), then .build().
HarnessConfig
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, default development), and the issue #137 key-ring keys: HARNESS_SECRET_REVOKED (optional, comma-separated key ids whose tokens must be refused immediately) and HARNESS_VENTURE (optional label binding tokens to this venture, so one venture’s links cannot verify in another that shares its secret by mistake).
HmacSigner
HMAC-SHA256 signer over a KeyRing (ADR 0006, ADR 0014).
HttpPolicy
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.
ImplicitTenant
The resolver a deployment with no control database uses: every host is the one venture, always active.
Inbox
A dedup ledger over the Database port. Construct it with the table the owning module declares (e.g. "<module>_inbox").
Json
A Json extractor and response whose rejections and serializations are problem+json (architecture section 6). Deserialization failures become a 400 validation-failed problem listing the field.
KeyRing
A bounded set of token-signing keys with explicit states (issue #137).
LineItem
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 Config backed by a map (tests, fz doctor with process env).
Member
A connected participant, identified by the venture after it verified the upgrade request’s bearer token (through the Signer or the auth client). A RoomHandler only ever sees this verified id, never a raw token.
Message
An outbound mail. text is always sent alongside html.
Migrations
The module’s migrations, per dialect. postgres differs from sqlite only where the SQL truly differs (ADR 0004).
ModuleConfig
Typed view over a Config for one module: prefixes every key with the module name in SCREAMING_SNAKE and parses values with defaults (issue #3).
ModuleContext
Everything a module’s router needs: its declared ports, the typed config, the bus, the templates and the venture identity.
ModuleSurface
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. currency is a lowercase ISO-4217 code ("usd").
NoopDefer
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. data is the custom key-value payload the app reads; collapse_id coalesces notifications the user has not seen yet.
Outbox
A durable work queue over the Database port. Construct it with the table the owning module declares (e.g. "<module>_outbox").
OutboxRecord
A leased outbox record handed to a drainer.
Payload
The signed payload: { purpose, subject, exp?, kid }.
PersonalDataCatalog
Every personal-data declaration in a composition, with the module that made it.
PersonalDataSet
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.
ProblemDef
Definition of one problem slug.
RedactingVisitor
A tracing field 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.
RefundRequest
A refund of a prior payment: the whole amount when amount is None, else a partial refund.
Rendered
A rendered mail, ready for Message.
RenderedSurface
A document serialized once, with the strong ETag clients revalidate against. Built at Harness::build for 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).
RoutingPush
Dispatches by Recipient variant to the adapter a venture configured for that transport, so venture code holds one Arc<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 Scope extractor; there is no ambient “current request”.
ScopedBlob
Wraps a Blob so every key is prefixed with <module>/ and no key can escape it. The harness applies this in Ports::view_for, so a module sees a store scoped to itself — the blob equivalent of the table-ownership check.
SendCooldown
A cooldown ledger over the Database port. Construct it with the table the owning module declares (e.g. "<module>_send_cooldown"); the owning module ships create_table_sql as a migration.
SidecarMount
One mounted sidecar.
SidecarMounts
The mount table, parsed from configuration.
SqlMigration
One migration step, embedded with include_str! from crates/<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 bind values positionally.
SubjectVia
One hop to the subject, through a table that holds the id requests are made with.
SubscriptionCheckoutRequest
A recurring hosted checkout (Stripe Checkout in subscription mode) against a Stripe Price the venture configured (e.g. $15/mo with a trial).
Surface
What a module declares from Module::surface.
SurfaceDocument
The document GET /__surface serves: composed at Harness::build, one entry per module in mount order, modules with an empty surface omitted.
SystemClock
Real wall clock (time crate). Its timeout runs futures to completion — tests that need a real timeout supply their own clock.
TemplateRegistry
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.
TenantConn
A database handle bound to the request’s tenant.
TenantId
A tenant’s key in the control database’s registry.
TokenPolicy
A per-purpose lifetime ceiling (ADR 0014, issue #137).
TransferCharge
A destination charge with an application fee: the buyer is charged amount, application_fee is kept by the platform, and the remainder is transferred to destination_account (the coach’s Connect account).
UiContext
What a UI renderer gets from the harness (ADR 0010). Built by Harness::router for every router it assembles.
UlidIdGen
Real ULID generator (monotonic per process).
Venture
Identity and CORS configuration for the venture this harness serves.
VentureSurface
The venture identity a renderer needs.
Verdict
A captcha verification verdict. ok: false with a reason from the provider’s error-codes; transport-level unavailability surfaces as ok: false, reason: "unavailable" unless the adapter is configured fail-open (staging only).
WebhookEvent
A webhook event the adapter has verified (signature + timestamp) before returning. kind is Stripe’s event type ("checkout.session.completed"); data is the event’s data.object for the module to interpret.
WriteGuards
What the composed modules demand of the runtime, module by module.

Enums§

Audience
Who an action is for. Drives the public/admin split of /__surface and, in the renderer, which pages need the admin session.
BlobError
Blob store failures.
CaptchaError
DataKind
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.
DispatchError
Disposition
What erasure does to these rows.
HttpError
KeyState
A key’s role in the ring (ADR 0014, issue #137).
Kid
Which key in the crate::KeyRing a token was signed with. Tokens name their key so rotation never breaks links in flight.
KvError
MailError
Mailer failures, mapped by the adapter from provider responses.
OriginError
Why a string is not an origin this harness will name.
Outcome
What the browser should do with a successful response.
PaymentsError
Payment failures. NotConfigured lets a venture build and run without Stripe (the port reports it rather than erroring); the rest map an upstream failure. SignatureInvalid is separated so a webhook handler answers 400 and never processes an unverified event.
Platform
The transport a Recipient is reached over, which is not a statement about the device: a UnifiedPush endpoint is Platform::Web on 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) and 5 (deliver to save power); to android.priority HIGH/NORMAL; to the Web Push Urgency header high/normal.
PushError
Push failures. Unregistered is 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.
PushOutcome
The result of a send that the provider accepted.
RateLimit
Outcome of consulting the limiter for one request.
RateLimitError
RateLimitFailure
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.
RealtimeError
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.
RoutePolicy
How a request to a declared route proves it is legitimate.
SendOutcome
Result of a send attempt.
SignatureError
Failures surfaced by verify beyond “the token is simply invalid”, which is reported as None.
SignerError
Errors from constructing an HmacSigner or a KeyRing.
TemplateError
TenantDbError
Why a tenant’s database could not be handed over. Deliberately carries the tenant id and nothing else — no DSN, no driver message.
TenantStatus
A tenant database’s reconciliation status.
VentureEnv
Deployment environment. Mirrors the ENV config key; Production drives 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_admin has 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::build rejects modules whose harness_api differs. 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_config rejects 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 under x-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 ScopedBlob or 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/offset query params) and clamped to this: an unbounded SELECT * 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: true tells the operator a next page exists.
MAX_KID_NAME
The longest accepted Kid::Named wire 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 people payloads) 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_SECRET must 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_SECRET stays 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 /__surface request must carry a valid gateway token minted with SIDECAR_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 ScopedBlob per 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
HttpClient
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 Recipient names. An adapter serves one transport and returns PushError::unsupported_recipient for the rest; RoutingPush fans a mixed set of recipients out across the adapters a venture configured.
RateLimiter
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 via Ports.
ResolveTenant
How a deployment turns a request’s host into a tenant.
RoomContext
The operations a RoomHandler performs 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.
RoomHandler
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 Ports and declares statically which ports it can provide, so Harness::build can 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).
SurfaceSource
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. data is the caller’s JSON payload; locale is the requested locale tag (en, de, …) used by the implementation for its own variants.
TenantDatabases
How a deployment turns a resolved tenant into a database handle.
TenantRouting
A deployment’s tenant plane: a host becomes a tenant, and a tenant becomes a database handle.
TryFromValue
Conversion from a sea-query SeaValue for typed row access.
UiMount
A renderer the venture mounts at /ui with HarnessBuilder::ui (ADR 0010). Core defines the seam; cratefield-ui is the implementation, kept out of core so a venture without a UI carries no maud.

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 Captcha port 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 text contains (case-insensitive), or None. 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). Every Blob::put implementation 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_failure at 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 sql leaves 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-Length of 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::build and fz doctor report for a module whose Module::harness_api differs from core’s: it names the module, the module crate’s version, the API it targets, and the cratefield-core crate 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: a select whose 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-failed problem 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 in sql.
migration_checksum
The sha256 of a migration’s SQL, lowercase hex. Recorded in harness_migrations when 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 the Payments bar: signature verification is the adapter’s per-request duty (verify_webhook + Inbox), and which secret it checks against is deploy configuration that fz doctor reads 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 ConfigError convention: report every problem at once). Empty for anything but Production; pass allow_no_captcha = Some(reason) (the fz doctor override) 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> (or ip:unknown when no address is visible), plus email:<normalized> when an address is known. The limiter is consulted per key, in order.
rate_limited
A 429 rate-limited problem carrying Retry-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 through scrub_text otherwise (issue #135: a secret inside a generic uri/message/error value 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 T the 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_hash uses 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 from HARNESS_SECRET and installs it once at startup; subject_hash then emits HMAC(key, email) instead. Unset (tests, or a runtime whose HARNESS_SECRET failed validation) makes subject_hash emit a fixed all-zero 000000000000 placeholder — 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::SignedLink route 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 is HMAC-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. None means the clock abandoned the future after after.
ttl_secs
A Notification::ttl as 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::tables never 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 HumanForm handlers (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 Future for use in cases where you can’t statically type your result or need to add some indirection.
EventHandler
A registered handler: receives the emitting request’s scope and the payload.
EventName
Event names are "<module>.<event>", e.g. waitlist.confirmed.