Event Model
The signed Woven event record — schema, deterministic IDs, identity binding, and on-disk encoding.
Purpose
This page covers the canonical Woven event: the Event struct, the EventBuilder constructor, the deterministic EventRef ID, Ed25519 signing, and the wire schema used when an event is stored in a Strand block. Read this first if you are defining a new event kind, parsing events from a peer, or implementing an identity adapter that signs on behalf of an author.
What an event is
An Event binds an author identity (DID or identity URI), a SpaceId, an event kind (dotted lowercase like social.post), optional refs, attachments, capabilities, a canonical JSON body, a created_at second-resolution timestamp, the signer's 32-byte Ed25519 public key, and an Ed25519 signature over the canonical payload. The EventRef ID is the hex digest of weave_crypto::hash(["woven:event:v1", canonical_payload]), so identical payloads always produce identical IDs.
EventRef is content-addressed. Two identical payloads from the same author produce the same EventRef — useful for deduplication, but it means you cannot publish two semantically distinct events with identical canonical bodies.
Signing an event
use serde_json::json;
use woven::{EventBuilder, SpaceId};
let keypair = weave_crypto::key_pair(Some(&[7u8; 32]));
let event = EventBuilder::new(
"did:l1fe:agent:alice",
SpaceId::new("woven://dsocial/global")?,
"social.post",
)
.body(json!({ "text": "hello p2p web" }))
.created_at(1_777_130_000)
.sign(&keypair)?;
assert!(event.verify(&keypair.public_key)?);EventBuilder rejects empty authors and event kinds that are not dotted lowercase (a-z0-9_- segments separated by .). Bodies are canonicalised (object keys sorted recursively) before signing so two clients producing the same logical body always agree on the ID.
Signing through an identity adapter
When the signing key lives in a wallet, HSM, or remote service, use sign_with to provide a closure that returns the signature bytes for a canonical payload:
let event = EventBuilder::new(
"did:l1fe:agent:alice",
SpaceId::new("woven://dsocial/global")?,
"social.post",
)
.body(json!({ "text": "signed by identity adapter" }))
.created_at(1_777_130_000)
.sign_with(keypair.public_key, |payload| {
Ok(weave_crypto::sign(payload, &keypair.secret_key).to_vec())
})?;The closure must produce a signature over exactly the bytes it is handed. Woven never re-canonicalises the payload after signing, so swapping in a different serializer in your adapter will break verification.
Verification and tampering
Event::verify(&public_key) recomputes the deterministic ID, checks that the supplied key matches event.public_key(), and validates the Ed25519 signature against the canonical payload. Any field mutation — including a single byte changed in body after signing — fails verification.
Storage encoding
Event::to_strand_bytes() wraps an event in a StrandEventRecord { protocol: "woven.event", version: 1, event } envelope and serialises it as JSON. The matching Event::from_strand_bytes() decoder rejects unknown protocols and versions, so the on-disk format is forward- and backward-compatible.
Primary types to know
Event— the signed recordEventBuilder— typed constructor with sign/sign_withEventRef— deterministicwoven:event:<hex>identifierSpaceId—woven://<app>/<scope>identifierWovenError— typed error enum withInvalidIdentifier,InvalidKind,Schema, andVerificationFailed