Skip to content

Events

EventBus listen/dispatch + catalog of first-party plugin events.

Author guide — edit docs/.vitepress/plugin-sdk-guides/events.md, then pnpm docs:generate.

In-process typed events (Event + EventBus) for plugins and apps. Listeners run synchronously in registration order inside dispatch. For async work, tokio::spawn / TaskBackend::dispatch from the listener.

rust
use sova::{Event, EventBus};

#[derive(Clone)]
struct NoteCreated { id: i64 }
impl Event for NoteCreated {
    fn name(&self) -> &'static str { "app.note_created" }
}

let bus = app.events(); // inserts EventBus into app.state on first call
bus.listen::<NoteCreated, _>(|e| {
    tracing::info!(note_id = e.id, "created");
});
// somewhere later:
app.events().dispatch(NoteCreated { id: 1 });

Soft-wire pattern in plugins: client.set_events(app.events()) / hold Option<EventBus> and dispatch after mutations (mail, fs, auth, …).

DevTools (feature-gated) mirrors many of these into the timeline — see devtools.


Built-in events (first-party plugins)

Stable string from Event::name(). Types live in the plugin crate (re-exported by facade features).

name()TypeCrate / featurePayload
auth.user_registeredUserRegisteredauthuser_id, email
auth.user_logged_inUserLoggedInauthuser_id, email
mail.sentMailSentmailto, subject
csrf.mismatchCsrfMismatchcsrfmethod, path
session.regeneratedSessionRegeneratedsessionhad_user
session.logout_allSessionLogoutAllsessionuser_id, count
rate_limit.exceededRateLimitExceededrate-limitkey, limit, retry_after
tasks.dispatchedTaskDispatchedtasksid, name, queue
tasks.failedTaskFailedtasksid, name, attempts
notifications.sentNotificationSentnotificationschannel, event, recipients
passport.api_token_revokedApiTokenRevokedpassportuser_id, token_id
acme.certificate_issuedCertificateIssuedacmedomains, not_after_unix
acme.certificate_renewedCertificateRenewedacmedomains, not_after_unix
acme.failedAcmeFailedacmedomains, error
fs.file_writtenFileWrittenfspath (relative to jail)
fs.file_removedFileRemovedfspath
fs.dir_createdDirCreatedfspath

Listen example against a first-party type:

rust
use sova::prelude::*;
use sova::{MailSent, UserRegistered};

// after plugins are installed — bus is shared
app.events().listen::<UserRegistered, _>(|e| {
    tracing::info!(id = e.user_id, email = %e.email, "registered");
});
app.events().listen::<MailSent, _>(|e| {
    tracing::debug!(?e.to, subject = %e.subject, "mail accepted");
});

Apps may define their own Event types (cabinet NoteCreated, etc.) — they are not in the table above.


Rules for new plugin events

  1. One Rust type per event; name() uses plugin.action snake style (auth.user_registered).
  2. Payload: cheap Clone fields (ids, strings) — no request bodies.
  3. Dispatch after the side effect succeeded (or immediately before a terminal error response for security signals like CSRF / rate-limit).
  4. Soft-dep on EventBus — do not require a separate plugin install.
  5. Optionally wire into DevTools hub behind a feature flag.

See also: Extractors & Problem+ · Lifecycle