Plugin trait with hooks for the events you care about, registered at startup. Most plugins are 50–150 lines of Rust. This page walks through writing one end-to-end.
When to write a plugin
| Need | Plugin? |
|---|---|
| One-off business logic | No — write a function |
| Field-level computed values via TS | No — use the computed plugin |
| Custom validation rule | No — use the validation plugin’s custom rule type |
| Webhook delivery | No — use the webhooks plugin |
| Cross-cutting behavior on every write | Yes — write a plugin |
| Replace a built-in subsystem (storage, email, cache) | Yes — write a plugin |
| Add new HTTP routes | Yes — write a plugin |
- Sync entity changes to an external CRM in real time
- Apply company-specific validation rules across many entities
- Add Slack notifications on any audit-log entry
- Implement a custom auth provider (Apple, Microsoft, Discord)
The Plugin trait
Example: a Slack-notifier plugin
We want every new sign-up to ping a Slack channel. Built-inwebhooks could do this, but suppose we want richer formatting and the Slack message format isn’t a stable webhook URL we control.
User insert pings Slack. Total: ~30 lines.
The WriteOp enum
before_write and after_write get the operation so you can target specific events.
Adding HTTP routes
For plugins that expose new endpoints (Stripe webhook receiver, OAuth callback for a custom provider, internal health check):Hooking the data layer
before_write lets you mutate data before it lands. Used by timestamps, slugify, computed, tenant_scope:
Err(PluginError::ValidationFailed { code, message }) rejects the write with a 400 response.
Per-request hooks
before_request runs before route dispatch. Used by rate_limit, cors, csrf:
PluginError::RateLimited { retry_after_secs } produces a 429 response with the right header.
Registering with the runtime
The runtime readsmanifest.plugins at boot and matches each entry’s name against built-in names. To add your own:
Performance guidance
- Hot-path plugins must be fast.
before_requestruns on every HTTP request — if it allocates or locks, it shows up at p99. after_writeshould defer expensive work. Spawn a thread or push to a queue rather than blocking the response.- Use
Mutexsparingly. For high-throughput counters, preferAtomicU64or sharded state. - Don’t call
.unwrap()in hooks. A panic in a plugin can crash the runtime thread; return errors instead.
Testing
Distributing
For internal use, ship plugins in your own runtime build (a fork ofcrates/runtime with register_plugin calls in main.rs).
For public plugins, publish a crate that exposes a register(runtime: &mut Runtime) function — users add it to their build’s dependencies and one-line-call it.
The community plugin registry is on the roadmap; until then, GitHub stars and word of mouth.
Reference: built-in plugins as examples
Thecrates/plugin/src/builtin/ directory has 30+ plugins across every shape — read them as reference:
- Simple after-write hook:
audit_log.rs,webhooks.rs - Before-write data mutation:
timestamps.rs,slugify.rs,computed.rs - Per-request gate:
rate_limit.rs,cors.rs,csrf.rs - HTTP routes:
api_keys.rs,totp.rs,stripe.rs - Subsystem replacement:
file_storage.rs,cache_client.rs,email.rs - External-service integration:
vector_search.rs,ai_proxy.rs,mcp.rs