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("factory0", "factory0.ventures")
        .public_url("https://factory0.ventures")
        .cors_origins(["https://factory0.ventures"]))
    .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/>phase 3")]:::future
  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-postgresPhase 3. Database over sqlx for the native runtime
cratefield-runtime-nativePhase 3. The same harness as a single binary on tokio

Private modules are fz-* crates in harness-private, consumed as pinned git dependencies. New ventures start from venture-backend-template. The first consumer is factory0-backend.

§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. Designed, not built: epic #56.

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 portabilityPostgres adapter, native runtime, parity suite, data move#18–#21

Three epics sit outside the milestones because they are specified but not scheduled: #23 multi-tenant schema, #24 embedded secrets, and #56 custom modules without rebuilding the shared bundle.

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:

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.
Brand
Venture branding for mail templates (issue #12). Defaults are text-only: factory-zero orange accent, no logo, no footer line.
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 (one Arc).
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).
HmacSigner
HMAC-SHA256 signer over HARNESS_SECRET (+ optional previous).
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.
LineItem
One line on a checkout: a name shown to the buyer and its price.
MapConfig
A Config backed by a map (tests, fz doctor with process env).
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. data is the custom key-value payload the app reads; collapse_id coalesces notifications the user has not seen yet.
Payload
The signed payload: { purpose, subject, exp?, kid }.
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.
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.
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.
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.
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.

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
DbError
Database failures, sanitized for logs and problem details.
DispatchError
HttpError
Kid
Which secret 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.
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.
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).
PushError
Push failures. Unregistered is separated because the caller must act on it — the device token 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.
RateLimitError
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.
TemplateError
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§

FORMULA_PREFIXES
Characters that make a cell a formula when it starts with one of them.
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_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.
MAX_BODY_BYTES
Default request body limit for /v1/* JSON endpoints.
MAX_EMAIL_BYTES
The maximum accepted address length in bytes (RFC 5321 “forward-path”).
MAX_LOCAL_BYTES
The maximum local-part length in bytes (RFC 5321).
MIN_SECRET_BYTES
Minimum secret length. HARNESS_SECRET must be at least 32 bytes.
SLUGS
SURFACE_API
Contract version of the surface document, independent of HARNESS_API: a renderer or the control plane checks it before reading the document.
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_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. APNs today, FCM later; both over the runtime’s HttpClient.
RateLimiter
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.
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§

bearer_token
Extracts the bearer token from Authorization: Bearer <token>.
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).
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.
csv_escape
Escapes one CSV field: formula-guard, then RFC 4180 quoting.
csv_row
Escapes and joins one CSV row, with a trailing newline.
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_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.
normalize_email
Trim, Unicode NFC normalise, then lowercase. Idempotent.
problem_registry
Every core slug definition, for tests and docs.
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 otherwise.
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.
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.
set_error_forwarder
Installs a process-wide forwarder for internal-error diagnostics (architecture section 11).
subject_hash
The redacted form of an email-ish value: its SHA-256 digest, 12 hex characters, no @ ever reaches the logs.
timeout
Typed wrapper over Clock::timeout_any. None means the clock abandoned the future after after.
validation_error
The reason an address is rejected, for problem details.

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.