timbl

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.

content.config.ts
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
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.

definePlugin
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.

Run it now.

Two commands. The default server listens on http://localhost:3000.

terminal
bun install
bun run dev