Not released yet. PeltyCMS is under development and not released yet; these docs describe the upcoming release and may change.
PeltyCMS Developer docs
User docsDevelopers
v0.1.0

Developer docs/Guides

Events and hooks

Reference: Events index (every event, payload and emit site), Cancel and modify (everything you can stop or change) and Hook points index.

PeltyCMS emits a domain event after something has happened, for example invoice.paid. Before a few sensitive actions it also runs a pre-event, for example order.before_place, which a hook can cancel or modify. Every name, payload field and flag is listed in the event registry. That registry is checked by a test that scans the source for every emitted event.

#Who can receive events

#Listening

extensions/examples/addon-guide/src/lib.rs · snippet listenRust
/// Domain events listed in `capabilities.events` arrive here after the fact.
/// They cannot be cancelled; use them to sync, notify or count.
fn on_event(name: &str, payload: &Value) {
    match name {
        "invoice.paid" => bump("paid"),
        "user.registered" => {
            bump("registered");
            let _ = host::kv_set("last_user", payload["user_id"].as_str().unwrap_or(""));
        }
        _ => {}
    }
}

#Cancelling and modifying

extensions/examples/addon-guide/src/lib.rs · snippet hooksRust
/// Pre-events and lifecycle phases listed in `capabilities.hooks` call op
/// `hook`. Return `proceed`, `modify(payload)` or `cancel(reason)`.
fn hook(input: &Value) -> Value {
    let event = input["event"].as_str().unwrap_or("");
    if input["phase"].as_str().unwrap_or("before") != "before" {
        return reply::proceed(); // during/after/finished only observe
    }
    let mut p = input["payload"].clone();
    bump(&format!("hook:{event}"));
    match event {
        // Cancel: refuse sign-in for one domain.
        "login.before" if p["email"].as_str().is_some_and(|e| e.ends_with("@denied.test")) => reply::cancel("domain not allowed"),
        // Modify: tidy up the display name before the account is created.
        "client.before_register" => {
            p["display_name"] = json!(p["display_name"].as_str().unwrap_or("").trim().to_string());
            reply::modify(p)
        }
        // Own custom event (declared as `before:integration-guide:greeted`,
        // delivered with `event` = the bare name and `phase` = "before").
        "integration-guide:greeted" if p["name"] == "nobody" => reply::cancel("no greeting for nobody"),
        "order.before_place" => order_hook(input, p),
        _ => reply::proceed(),
    }
}

A hook replies with one of the following:

#Lifecycle phases

Phases in order: before, during, after, finished. Source: crates/pelty-api/src/addon_events.rs.

#Custom events

An addon can emit <its-own-id>:<event> if the name is listed in capabilities.emits. The before phase runs synchronously through every listener, in priority order. Other addons subscribe with before:<id>:<event> (and after: or finished:) in their hooks. An addon cannot emit an event in another addon's namespace.

extensions/examples/addon-guide/src/lib.rs · snippet emitRust
/// Emit a custom event (`capabilities.emits`, name `<addon-id>:<event>`).
/// The `before` phase runs synchronously, so listeners can cancel or modify it.
fn greet(name: &str) -> Result<Value, String> {
    let e = pelty_addon_sdk::emit("integration-guide:greeted", &json!({ "name": name }))?;
    if e.cancelled {
        return Ok(json!({ "status": 409, "body": { "error": e.reason } }));
    }
    bump("greeted");
    Ok(json!({ "status": 200, "body": { "greeted": e.payload["name"] } }))
}