Architecture

How mixin-style hook pipelines compose across plugins.

Mixin-style hook pipeline

FeatherFly plugins extend the daemon without recompiling it, similar to how Minecraft mixins inject behavior into existing code paths.

Daemon startup
    │
    ├─ load .so files (alphabetical)
    │       └─ init(host) ── register all hooks
    │
    ├─ config.mutate pipeline ──► [plugin A] ──► [plugin B] ──► final YAML
    │
    ├─ config.loaded (post-mutation YAML bytes)
    │
    └─ HTTP listening
            │
            ├─ request.intercept ──► middleware.inject ──► handler
            │
            ├─ plugin routes (route.register)
            │
            ├─ CloudPanel API ──► cloudpanel.command ──► clpctl
            │
            └─ JSON response for /api/*
                    ├─ json.response pipeline
                    └─ json.actions pipeline

How a JSON mixin chain works

  1. FeatherFly builds the base JSON response for a route.
  2. Each plugin registered for json.response on that route prefix runs in load order.
  3. Plugin 1 receives the original JSON, may mutate it, writes output.
  4. Plugin 2 receives plugin 1's output, may mutate again.
  5. After all body hooks, json.actions hooks run on the actions array only.
  6. The final JSON is sent to the client.

This is intentionally composable: small plugins each do one job (add a field, inject a step, strip sensitive data) instead of one monolithic plugin patching everything.

Lifecycle vs mutation

Hook typeWhen it runsCan cancel others?Typical use
Lifecycle eventFixed daemon milestonesYes (same event only)Startup banners, config validation
JSON mutationEvery matching HTTP JSON responseNo — always runs in pipelineResponse fields, panel actions
CloudPanel commandBefore clpctl process spawnYes — cancels command executionPolicy checks, defaults

Versioning

Plugin API v8 (current) exposes lifecycle events, config mutation, request hooks, plugin routes, JSON mutation, and CloudPanel command hooks. Future hooks will bump the API version so old plugins keep loading safely when new hook types appear.

HTTP request stack

HTTP request stack

Client request
    │
    ▼
response_middleware (pass-through on request; mutates JSON on response)
    │
    ▼
request.intercept hooks (outer — auth, rate limits, early reject)
    │
    ▼
middleware.inject hooks (inner — tracing, header checks)
    │
    ▼
handle_request (debug logging)
    │
    ▼
Axum router (core routes + plugin routes)

Request hooks receive method, path, headers as JSON, and body bytes. Headers use lowercase keys. authorization is redacted.

Route patterns use prefix matching: /api/* matches /api/system, /plugins/hello matches exactly that path prefix chain.

Plugin routes register during init and merge into the same router as core API routes.

See also request hooks, config hooks, and hooks roadmap.