Shuttle Plugin Interface
Exact types and contracts for the Shuttle plugin system, from pkg/shuttle/.
Interface
type Shuttle interface {
Name() string // stable identifier; logs and conflict detection
Version() string // informational; semver
Init(r *Registry) error // called once at boot
}
Init runs before any HTTP traffic. Returning a non-nil error aborts server boot — use this for missing required env vars or bad config.
Registry
type Registry struct { /* opaque */ }
func NewRegistry() *Registry
func (r *Registry) AddRoute(method, pattern string, h http.Handler) error
func (r *Registry) AddHook(model string, event HookEvent, fn HookFunc) error
func (r *Registry) Routes() []Route
func (r *Registry) Hooks() []Hook
func (r *Registry) HooksFor(model string, event HookEvent) []Hook
Append-only collector. Plugins call AddRoute / AddHook from inside Init; the runtime reads the Routes and Hooks slices afterwards.
AddRoute validates the obvious shape errors at registration: empty method, relative pattern, nil handler.
AddHook validates: empty model, unknown event, nil function.
Route
type Route struct {
Method string // GET, POST, PUT, DELETE, …
Pattern string // chi syntax, absolute (/webhooks/stripe)
Handler http.Handler // any standard http.Handler
Source string // populated by Load — the plugin's Name()
}
Plugin routes mount on the root router, outside /api. They do not pass through the JWT auth chain — webhooks and OAuth callbacks belong here. If a plugin route needs user auth, the plugin wraps its own middleware.
Hooks
type HookEvent string
const (
BeforeSave HookEvent = "before_save"
AfterSave HookEvent = "after_save"
BeforeDelete HookEvent = "before_delete"
AfterDelete HookEvent = "after_delete"
)
type HookFunc func(ctx context.Context, record map[string]any) error
type Hook struct {
Model string
Event HookEvent
Fn HookFunc
Source string
}
Hook chains run synchronously, in registration order. Mutations to record by Before* hooks are persisted; mutations by After* hooks are no-ops.
Errors from Before* hooks return HTTP 400 with the error message in the body. Errors from After* hooks log at warn level (plugin=<name> event=<event> err=<err>) but the HTTP response is unaffected.
Loading
func Load(shuttles []Shuttle) (*Registry, error)
The runtime calls this once at boot. Behaviour:
- Iterates plugins in input order
- Calls each plugin's
Initwith a single shared Registry - Stamps every registered Route and Hook's
Sourcefield with the plugin'sName() - Refuses duplicate plugin names (programming error)
- Refuses empty plugin names
Load returns the first error encountered, with the responsible plugin's name in the error message. The runtime aborts boot on any error — broken plugin = no startup, not a half-running process.
Wiring into the server
runtime.NewServer(runtime.ServerOptions{
DB: db,
Threads: threads,
Auth: auth,
Plugins: []shuttle.Shuttle{
myPlugin,
anotherPlugin,
},
})
NewServer calls shuttle.Load and returns its error if any. Hooks become available to ResourceHandler automatically — no extra wiring.
What plugins can NOT do (in v0.1.0)
- Add custom field types — would require parser hooks
- Add custom CLI commands — would require Cobra integration on the loom binary itself
- Replace built-in handlers — only additive routes; no overriding
/api/... - Run dynamically — Go imports only, no
.soplugins
These are conscious omissions. The current surface keeps the runtime predictable and type-safe.
See also
- Concepts → Plugins (Shuttle) — overview and metaphor
- Tutorials → Adding a Stripe Webhook with Shuttle — full example