Skip to content

Extension Model

Two ways to extend the runtime. Plugins are the supported, portable path. The unsafe namespace is the escape hatch for cases the plugin surface cannot cover.

Plugins

A plugin is a plain object created with definePlugin. It runs at runtime initialization and can extend schema, register routes, add hooks, register query extensions, and contribute OpenAPI fragments.

import { definePlugin } from "timbl";
export const myPlugin = definePlugin({
name: "my-plugin",
extend: {
collections: {
posts: [{ key: "readingTime", type: "number" }],
},
},
setup({ registerRoute }) {
registerRoute({
method: "GET",
path: "/api/my-plugin/status",
auth: "public",
handler() {
return { status: 200, body: { ok: true } };
},
});
},
});

Register a plugin in defineCMS({ plugins: [myPlugin] }). See the Plugins guide for the full API: schema extension, routes, hooks, query extensions, filters, serializers, and OpenAPI contributions.

What plugins can do

  • Add fields to existing collections and globals (extend)
  • Define new collections and globals (collections, globals)
  • Register custom field types (fields)
  • Register HTTP routes (routes or setup with registerRoute)
  • Attach hooks to any collection or global lifecycle stage (hooks)
  • Register named query extensions (queries or registerQuery)
  • Transform handler inputs (filters) and API output (serializers)
  • Merge OpenAPI fragments into the generated spec (openApi)

What plugins cannot do

Plugins cannot reach into the database driver, the HTTP app instance, or adapter internals directly. If you need that, you use the escape hatch or write a custom adapter instead.

Adapters

Adapters replace infrastructure components: database, auth, storage, and HTTP. The runtime ships with defaults for each (SQLite with Drizzle, Better Auth, local storage, Hono), but every adapter is replaceable through a documented contract.

  • DatabaseAdapter: persistence. The default wraps SQLite with Drizzle. See Custom Adapters.
  • AuthAdapter: authentication and principal resolution. The default is Better Auth. See Authentication.
  • StorageAdapter: file uploads and asset URLs. The default is local storage. See Storage and Uploads.
  • HttpAdapter: route registration and the listen call. The default is Hono.

Adapter contracts are stable when used through their documented factory functions (defineAuthAdapter, etc.). The raw interface types in @timbl/core are advanced.

The escape hatch

The unsafe namespace exposes the database driver, the HTTP app, and other internals that the runtime normally wraps behind adapters. It exists for one-off migrations, driver-specific queries, and custom health checks that the documented surface cannot express.

Code that depends on unsafe is coupled to implementation details, not contracts. It can break in any release, including patches. See API Tiers for the full stability contract.

See also

  • Plugins: the full plugin API with examples
  • Custom Adapters: implementing DatabaseAdapter, AuthAdapter, StorageAdapter, and HttpAdapter
  • Hooks: hook stages, context, and execution model
  • API Tiers: stability contracts for each surface