# folk-api
Plugin contract for the Folk PHP application server.
> **Status:** in active development. See [folk-spec](https://github.com/Folk-Project/folk-spec) for the roadmap.
## Requirements
- Rust 1.85+
- Tokio async runtime
## Installation
```toml
# Cargo.toml
folk-api = "0.1"
```
## Quick start
A minimal plugin that logs on boot and shutdown (~25 lines):
```rust
use anyhow::Result;
use async_trait::async_trait;
use folk_api::{
Plugin, PluginContext, PluginFactory, ServerPlugin, ServerPluginWrapper,
RpcMethodDef,
};
use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, Deserialize, Default)]
struct GreetConfig {
message: Option<String>,
}
struct GreetPlugin {
message: String,
}
#[async_trait]
impl ServerPlugin for GreetPlugin {
fn name(&self) -> &'static str {
"greet"
}
async fn run(&self, ctx: PluginContext) -> Result<()> {
tracing::info!(msg = %self.message, "greet plugin running");
let mut shutdown = ctx.shutdown;
let _ = shutdown.changed().await;
tracing::info!("greet plugin stopping");
Ok(())
}
}
struct GreetFactory;
impl PluginFactory for GreetFactory {
fn create(&self, config: Value) -> Result<Box<dyn Plugin>> {
let cfg: GreetConfig = serde_json::from_value(config).unwrap_or_default();
let message = cfg.message.unwrap_or_else(|| "Hello, Folk!".into());
Ok(Box::new(ServerPluginWrapper::new(GreetPlugin { message })))
}
}
/// Required entry point — the builder calls this by name.
pub fn folk_plugin_factory() -> Box<dyn PluginFactory> {
Box::new(GreetFactory)
}
```
Register it in `folk.build.toml`:
```toml
[[plugin]]
crate_name = "my_greet_plugin"
path = "../my-greet-plugin"
config_key = "greet"
```
And configure it in `folk.toml`:
```toml
[greet]
message = "Howdy"
```
## Configuration
Plugins receive their config section as an opaque `serde_json::Value`. Each plugin deserializes it into its own struct. There is no global schema — the plugin owns its config shape.
## How it works
### Plugin lifecycle
1. **Factory** — The builder calls `folk_plugin_factory()` once per plugin crate. The returned `PluginFactory` receives the plugin's config as JSON and constructs a `Box<dyn Plugin>`.
2. **Boot** — `plugin.boot(ctx)` is called in registration order at server startup. Returning `Err` is fatal: already-booted plugins are shut down in reverse order and the server exits.
3. **Run** — For `ServerPlugin` implementations, `run()` executes as a long-lived task. Watch `ctx.shutdown` to know when to stop.
4. **Shutdown** — `plugin.shutdown()` is called in reverse registration order. Errors are logged but do not block the shutdown sequence.
### PluginContext
Every plugin receives a `PluginContext` at boot:
| `executor` | `Arc<dyn Executor>` | Send work to the PHP worker pool |
| `shutdown` | `watch::Receiver<bool>` | Fires when the server is shutting down |
| `rpc_registrar` | `Option<Arc<dyn RpcRegistrar>>` | Register admin RPC methods |
| `health_registry` | `Option<Arc<dyn HealthRegistry>>` | Register health checks |
| `metrics_registry` | `Option<Arc<dyn MetricsRegistry>>` | Register Prometheus metrics |
Optional registries are `None` when the corresponding plugin (metrics, etc.) is not loaded. Check before use.
### Key traits
- **`Plugin`** — 3 required methods: `name()`, `boot()`, `shutdown()`. Optional: `rpc_methods()`.
- **`ServerPlugin`** — Convenience trait for the common "spawn a task, wait for shutdown" pattern. Wrap with `ServerPluginWrapper` to get a `Plugin`.
- **`PluginFactory`** — Single method `create(config: Value) -> Result<Box<dyn Plugin>>`.
- **`Executor`** — `async fn execute(&self, payload: Bytes) -> Result<Bytes>`. Sends MessagePack-encoded payloads to the PHP worker pool and returns the response.
### RPC methods
Advertise methods from `rpc_methods()` and register handlers via `rpc_registrar.register_raw(name, handler)`. Handlers receive and return `Bytes` (typically MessagePack-encoded).
### Health checks
Register via `health_registry.register(name, check_fn)`. Return `HealthStatus::ok()` or `HealthStatus::degraded(msg)`.
### Metrics
Create metric families via `metrics_registry.counter_vec(name, help, &labels)`, then `.with_labels(&values)` to get a handle. Supports counters, gauges, and histograms. All metrics render in Prometheus text format.
See [ADR 0006 — Plugin API shape](https://github.com/Folk-Project/folk-spec/blob/main/adr/0006-plugin-api-shape.md) for the full design rationale.
## License
MIT