⚠️ Work in progress — createCMS is pre-1.0 and not production-ready (not tested in production). Expect breaking changes.
createCMS

Quickstart

Go from install to your first published, rendered page in a few minutes.

By the end of this guide you will have a pages collection, a generated database schema, a mounted HTTP router, and one published page that you read back and render. This is the loop you repeat for any collection: define, create the CMS, generate the schema, mount, create and publish content, then read it.

This is the guided happy path. For options and the full API, follow the links to Concepts and Reference instead of detouring here.

1. Define collections

A collection is a typed root plus a set of child blocks. Define one and group it with defineCollections:

lib/collections.ts
import { defineCollection, defineCollections } from '@createcms/core';

const pages = defineCollection({
  label: 'Pages',
  slug: { enabled: true, prefix: '/', nested: true },
  root: {
    properties: {
      title: { type: 'string', required: true, label: 'Title' },
    },
  },
  blocks: {
    hero: {
      label: 'Hero',
      properties: {
        headline: { type: 'string', required: true, label: 'Headline' },
      },
    },
    richText: {
      label: 'Rich text',
      properties: {
        content: { type: 'richText', required: true, label: 'Content' },
      },
    },
  },
});

export const collections = defineCollections({ pages });

2. Create the CMS

Pass your Drizzle db, the collections, and a media configuration to createCMS. The CLI in the next step discovers this file, so create it first:

lib/cms.ts
import { createCMS } from '@createcms/core';
import { db } from './db';
import { collections } from './collections';

export const cms = createCMS({
  db,
  collections,
  // Where `createcms generate` writes the Drizzle schema (default: `./cms-schema.ts`).
  schema: { output: './db/schema.ts' },
  media: {
    provider: 'digitalOcean',
    region: 'fra1',
    bucketName: process.env.S3_BUCKET!,
    accessKeyId: process.env.S3_ACCESS_KEY_ID!,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
    publicUrl: process.env.S3_PUBLIC_URL!,
  },
  authMiddleware: async () => {
    return { userId: 'system' };
  },
});

authMiddleware decides who may call the API. The { userId: 'system' } stub authorizes every request, including authoring calls from the browser. Replace it with real auth before production.

3. Generate and apply the schema

Generate the Drizzle schema with the CLI, then apply it with Drizzle Kit. Drizzle Kit reads its own config file, so create one at your project root first:

drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './db/schema.ts',
  out: './db/migrations',
  dialect: 'postgresql',
  dbCredentials: { url: process.env.DATABASE_URL! },
});

schema here must match where createcms generate writes. Step 2 set schema.output: './db/schema.ts', so both point at ./db/schema.ts. Without that option createcms generate defaults to ./cms-schema.ts — keep the two in sync.

With the config in place, generate the schema and apply it:

npx createcms generate
npx drizzle-kit generate && npx drizzle-kit migrate

The schema is content-agnostic: it is the core versioning tables (roots, commits, block_versions, and more) plus any plugin schema. Your collections never appear in it, so you re-run generate only when you add or change a plugin that ships schema, not when you edit a collection.

4. Mount the HTTP router (Next.js)

cms.router exposes a handler you wire into a catch-all route:

app/api/cms/[[...rest]]/route.ts
import { cms } from '@/lib/cms';

// Recommended when realtime is enabled: keeps the `/realtime` SSE stream
// unbuffered so live push is not cached or batched by the framework.
export const dynamic = 'force-dynamic';

const { handler } = cms.router;
export const GET = handler;
export const POST = handler;

5. Create and publish a page

Content does not exist until you create and publish it. Create a root, add a block on its main branch, then publish:

const page = await cms.api.pages.createRoot({
  body: { slug: 'welcome', properties: { title: 'Welcome' } },
});

await cms.api.pages.createBlock({
  body: {
    rootId: page.rootId,
    branchId: page.branchId,
    parentBlockId: page.rootId,
    type: 'hero',
    properties: { headline: 'Hello from @createcms/core' },
  },
});

await cms.api.pages.publishBranch({
  body: { rootId: page.rootId, branchId: page.branchId },
});

6. Read and render

Server-side, read the published page and render its block tree with BlocksRenderer. getPublishedContent returns one entry per published branch in variants; with no A/B test there is exactly one, so variants[0] is the page:

app/page.tsx
import { cms } from '@/lib/cms';
import { BlocksRenderer, createBlocksMap } from '@createcms/core/react/blocks';
import { collections } from '@/lib/collections';

const pageBlocks = createBlocksMap(collections.pages, {
  hero: ({ properties }) => <h1>{properties.headline}</h1>,
  richText: ({ properties }) => (
    // `richText` stores markup; render it as HTML (sanitize if untrusted).
    <div dangerouslySetInnerHTML={{ __html: properties.content }} />
  ),
});

export default async function Page() {
  const { variants } = await cms.api.pages.getPublishedContent({
    query: { path: '/welcome' },
  });
  return <BlocksRenderer blocks={pageBlocks} tree={variants[0].tree} />;
}

7. Use the type-safe client

On the client, createCMSClient<typeof cms> mirrors the server API. Every field is typed from your collection definition:

lib/cms-client.ts
import { createCMSClient } from '@createcms/core';
import type { cms } from '@/lib/cms';

export const cmsClient = createCMSClient<typeof cms>({ baseURL: '/api/cms' });

const { roots } = await cmsClient.pages.listRoots();
roots[0]?.properties.title; // typed as `string`

What's next

On this page