Skip to content

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 | ALL
  • path: string (used as-is; e.g. /api/health, /api/my-plugin/status)
  • auth: optional public | session (default treated as requiring session where applicable)
  • handler(context): returns { status, body } or Promise<...>

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-seocreateSeoPlugin({ collections?, globals? }) adds SEO fields.
  • @timbl/plugin-rsscreateRssPlugin(...) 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