Plugins
Plugins are how you extend the runtime. They add fields, routes, hooks, query extensions, filters, serializers, and OpenAPI fragments. Define one with definePlugin from timbl.
Minimal plugin
import { definePlugin } from "timbl";
export const healthPlugin = definePlugin({ name: "health", setup({ registerRoute }) { registerRoute({ method: "GET", path: "/api/health", auth: "public", handler() { return { status: 200, body: { ok: true } }; }, }); },});Register in defineCMS({ plugins: [healthPlugin] }).
Schema extension
collections/globals: full collection/global definitions owned by the plugin.extend: add fields to existing collections/globals:extend: { collections: { posts: [{ key: "readingTime", type: "number" }] } }.fields: register custom field types ({ type: "seoTitle", ... }with validation/serialization in the same plugin).
Routes
routes: [...] or setup({ registerRoute }) — each route:
method:GET|POST|PUT|PATCH|DELETE|ALLpath: string (used as-is; e.g./api/health,/api/my-plugin/status)auth: optionalpublic|session(default treated as requiring session where applicable)handler(context): returns{ status, body }orPromise<...>
context is RouteContext: includes request, cms (registry + runtime), auth, logger, params, query, optional unsafe.
Hooks
hooks: { beforeValidate, afterValidate, beforeChange, afterChange, beforeDelete, afterDelete, beforeOperation } — same stages as collection hooks. Prefer schema-level hooks for collection-specific logic; use plugin hooks for cross-cutting behavior.
Query extensions
Declared as queries: [{ name, register }]. After all plugin setup functions run, each register(context) is called with PluginSetupContext. The returned object’s keys are merged into the runtime extensions map (later keys overwrite earlier ones if names collide).
queries: [ { name: "featuredPosts", register: ({ query }) => ({ loadFeaturedPosts: () => query.collection("posts").findMany({ filters: [{ field: "featured", operator: "eq", value: true }], sort: [], }), }), },],Prefer setup + registerQuery when you only need to stash one helper by name without returning a batch object.
Filters & serializers
- Filters:
filters: [{ name, apply(input, context) }]— transform handler inputs. - Serializers:
serializers: [{ target: "collection:posts", serialize(record, context) }]— shape API output.
OpenAPI
openApi: [{ path, pathItem, schemas? }] merges into the generated spec.
Setup hook
setup(context) runs at runtime initialization; use registerRoute, registerHook, registerQuery, registerFilter, registerSerializer, registerOpenApi for dynamic registration.
Official plugins
@timbl/plugin-seo—createSeoPlugin({ collections?, globals? })adds SEO fields.@timbl/plugin-rss—createRssPlugin(...)for RSS feeds.
Contract tier
Plugin definition shape and RouteContext are stable. Low-level hook runner internals and registry build details are internal.
See also
- Hooks: hook stages, context, and execution model
- Extension Model: where plugins sit in the extension surface
- SEO Plugin and RSS Plugin: reference implementations
- Configuration: how plugins wire into
defineCMS