Schema-defined content modeling without the framework tax.
timbl owns content modeling, validation, persistence, and the generated API. It does not own your frontend. Define a config, stand up the server, ship content endpoints in minutes.
import { defineCMS, defineCollection, defineGlobal } from "timbl";
export default defineCMS({
collections: [
defineCollection({
key: "posts",
labels: { singular: "Post", plural: "Posts" },
fields: [
{ key: "title", type: "text", required: true },
{ key: "slug", type: "slug", required: true },
{ key: "body", type: "markdown" },
],
}),
],
globals: [
defineGlobal({
key: "siteSettings",
label: "Site Settings",
fields: [
{ key: "siteName", type: "text" },
{ key: "siteUrl", type: "text" },
],
}),
],
}); The schema is the source of truth.
Collections and globals are defined in TypeScript, validated at startup, and enforced everywhere: the generated API, the persistence layer, the client. No drift between what you wrote and what runs.
Fields are typed. Labels are declared. Required is explicit. The config reads like a spec because it is one.
defineCollection({
key: "posts",
labels: { singular: "Post", plural: "Posts" },
fields: [
{ key: "title", type: "text", required: true },
{ key: "slug", type: "slug", required: true },
{ key: "body", type: "markdown" },
],
}); Plugins are the only public seam.
Extension is plugins plus adapter contracts. Register routes, extend fields, swap storage, wire auth. Internal runtime wiring and implementation details are not public API. The extension surface stays small on purpose. A health check is six lines. A custom adapter is one contract. No monkey-patching, no service locator, no hidden globals.
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 } };
},
});
},
}); Strong defaults, framework-agnostic at the boundary.
Official defaults are opinionated. Every seam is overrideable through documented adapter contracts. The boundary stays framework-agnostic; the runtime ships with opinions.
- Layer
- Default
- Override
- Runtime
- Bun
- Adapter: runtime
- HTTP delivery
- Hono-shaped
- Adapter: server
- Persistence
- SQLite + Drizzle
- Adapter: persistence
- Storage
- Local filesystem
- Adapter: storage
- Auth
- Better Auth
- Adapter: auth
v1 is headless only. Admin UI, visual builders, durable job queues, image processing: out of scope, by design.
Six packages. One runtime.
- timbl Primary runtime and config authoring
- @timbl/core Contracts, plugin types, query surface, adapter interfaces
- @timbl/client Typed framework-agnostic REST client
- @timbl/adapter-better-auth Official Better Auth integration
- @timbl/plugin-rss Official RSS plugin
- @timbl/plugin-seo Official SEO field-extension plugin
Run it now.
Two commands. The default server listens on http://localhost:3000.
bun install
bun run dev