Getting started

Quick start with Void

Mount the CMS router in a Void app, run it locally, and create the first admin.


Install the package next to Hono in a Void app:

bun add @interactive-inc/cms hono drizzle-orm zod

Mount the router

createCMS returns { engine, router }. Mount the router under /api behind a middleware that sets c.set("user", { id, role }). A Bearer token takes the API key path; otherwise the session from void/auth applies.

// src/lib/build-app.ts
import { createCMS, verifyApiKey, type CMSEnv } from "@interactive-inc/cms"
import { bearerToken } from "@interactive-inc/cms/api-key"
import { Hono } from "hono"
import { getUser } from "void/auth"
import { db } from "void/db"
import { storage } from "void/storage"

export function buildApp() {
  return new Hono<CMSEnv>()
    .basePath("/api")
    .use("*", async (c, next) => {
      const token = bearerToken(c.req.header("authorization"))
      const user = token ? await verifyApiKey({ db, token }) : getUser()
      c.set("user", user ? { id: user.id, role: "role" in user ? String(user.role) : "editor" } : null)
      await next()
    })
    .route("/", createCMS({ db, bucket: storage }).router)
}

Then hand every /api request to it from a Void route:

// routes/api/[...route].ts
import { defineHandler } from "void"
import { buildApp } from "@/lib/build-app"

const dispatch = defineHandler((c) => buildApp().fetch(c.req.raw, c.env, c.executionCtx))

export const GET = dispatch
export const POST = dispatch
export const PUT = dispatch
export const PATCH = dispatch
export const DELETE = dispatch

createCMS is cheap, so build it per request when the D1 session is per request. On its first call the engine creates its own tables (cms_* and media) with CREATE ... IF NOT EXISTS and tracks its schema version in cms_meta. Do not add these tables to your own Drizzle schema or migrations.

Run it locally

Start the Vite dev server (bun run dev, or through portless for an https name). Void applies pending migrations and its auth tables on start. The reference app runs at https://cms.open.localhost.

Create the first admin

Sign-up is closed in the reference app. With OPEN_CMS_INIT_KEY set in .env and no users yet, sign up once with the key in the x-init-key header. The first account becomes admin.

curl -X POST https://cms.open.localhost/api/auth/sign-up/email \
  -H "x-init-key: $OPEN_CMS_INIT_KEY" \
  -H "content-type: application/json" \
  -d '{"email":"you@example.com","password":"your password","name":"You"}'

Sign in at /admin, invite other users from /admin/users, and issue an API key at /admin/api-keys to connect an AI agent (see MCP server).