Keystatic CMS Setup for the Next.js Supabase SaaS Kit

Configure Keystatic as your CMS with local file storage for development or GitHub integration for production and team collaboration.

Keystatic is a file-based CMS that stores content as Markdown/Markdoc files. It's the default CMS in Makerkit because it requires zero setup for local development and integrates with Git for version-controlled content.

Storage Modes

Keystatic supports three storage modes:

ModeStorageBest ForEdge Compatible
localLocal filesystemDevelopment, solo projectsNo
githubGitHub repositoryProduction, team collaborationYes
cloudKeystatic CloudManaged hostingYes

Local mode reads files directly from disk. GitHub mode fetches content via the GitHub API, making it compatible with edge runtimes like Cloudflare Workers.

Local Storage (Default)

Local mode works out of the box. Content lives in your repository's content/ directory:

# .env (optional - these are the defaults)
CMS_CLIENT=keystatic
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=local
KEYSTATIC_PATH_PREFIX=apps/web
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content

Content structure:

apps/web/content/
├── posts/ # Blog posts
├── documentation/ # Docs (supports nesting)
└── changelog/ # Release notes

Limitations: Local mode doesn't work with edge runtimes (Cloudflare Workers, Vercel Edge) because it requires filesystem access. Use GitHub mode for edge deployments.

GitHub Storage

GitHub mode fetches content from your repository via the GitHub API. This enables edge deployment and team collaboration through Git.

1. Set Environment Variables

# .env
CMS_CLIENT=keystatic
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=github
NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO=your-org/your-repo
KEYSTATIC_GITHUB_TOKEN=github_pat_xxxxxxxxxxxx
KEYSTATIC_PATH_PREFIX=apps/web
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content

2. Create a GitHub Token

  1. Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
  2. Create a new token with:
    • Repository access: Select your content repository
    • Permissions: Contents (Read-only for production, Read and write for admin UI)
  3. Copy the token to KEYSTATIC_GITHUB_TOKEN

For read-only access (recommended for production):

KEYSTATIC_GITHUB_TOKEN=github_pat_xxxxxxxxxxxx

3. Configure Path Prefix

If your content isn't at the repository root, set the path prefix:

# For monorepos where content is in apps/web/content/
KEYSTATIC_PATH_PREFIX=apps/web
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content

Keystatic Cloud

Keystatic Cloud is a managed service that handles GitHub authentication and provides a hosted admin UI.

# .env
CMS_CLIENT=keystatic
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=cloud
KEYSTATIC_STORAGE_PROJECT=your-project-id

Get your project ID from the Keystatic Cloud dashboard.

Adding the Admin UI

Keystatic includes a visual editor for managing content. To add it:

turbo gen keystatic

This creates a route at /keystatic where you can create and edit content.

GitHub Mode Admin Setup

GitHub mode requires a GitHub App for the admin UI to authenticate and commit changes.

  1. Install the Keystatic GitHub App on your repository
  2. Follow the Keystatic GitHub mode documentation for setup

The admin UI commits content changes directly to your repository, triggering your CI/CD pipeline.

Default Collections

Makerkit configures three collections in packages/cms/keystatic/src/keystatic.config.ts:

Posts

Blog posts with frontmatter:

---
title: "Getting Started with Makerkit"
description: "A guide to building your SaaS"
publishedAt: 2025-01-15
status: published
categories:
- tutorials
tags:
- getting-started
image: /images/posts/getting-started.webp
---
Content here...

Documentation

Hierarchical docs with ordering and collapsible sections:

---
title: "Authentication"
label: "Auth" # Short label for navigation
description: "How authentication works"
order: 1
status: published
collapsible: true
collapsed: false
---
Content here...

Documentation supports nested directories. A file at documentation/auth/sessions/sessions.mdoc automatically becomes a child of documentation/auth/auth.mdoc.

Changelog

Release notes:

---
title: "v2.0.0 Release"
description: "Major update with new features"
publishedAt: 2025-01-10
status: published
---
Content here...

Adding Custom Collections

Edit packages/cms/keystatic/src/keystatic.config.ts to add collections:

packages/cms/keystatic/src/keystatic.config.ts

// In getKeystaticCollections()
return {
// ... existing collections
pages: collection({
label: 'Pages',
slugField: 'title',
path: `${path}pages/*`,
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
description: fields.text({ label: 'Description' }),
content: getContentField(),
status: fields.select({
defaultValue: 'draft',
label: 'Status',
options: statusOptions,
}),
},
}),
};

Content Format

Keystatic uses Markdoc, a Markdown superset with custom components.

Basic Markdown

Standard Markdown syntax works:

# Heading
Paragraph with **bold** and *italic*.
- List item
- Another item
```code
Code block
### Images
Images are stored in `public/site/images/` and referenced with the public path:
```markdown
![Alt text](/site/images/screenshot.webp)

Custom Components

Makerkit extends Markdoc with custom nodes. Check packages/cms/keystatic/src/markdoc-nodes.ts for available components.

Cloudflare Workers Compatibility

Cloudflare Workers don't send the User-Agent header, which the GitHub API requires. Add this workaround to packages/cms/keystatic/src/keystatic-client.ts:

packages/cms/keystatic/src/keystatic-client.ts

// Add at the top of the file
const self = global || globalThis || this;
const originalFetch = self.fetch;
self.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const requestInit: RequestInit = {
...(init ?? {}),
headers: {
...(init?.headers ?? {}),
'User-Agent': 'Cloudflare-Workers',
}
};
return originalFetch(input, requestInit);
};

Environment Variables Reference

VariableRequiredDefaultDescription
CMS_CLIENTNokeystaticCMS provider
NEXT_PUBLIC_KEYSTATIC_STORAGE_KINDNolocalStorage mode: local, github, cloud
NEXT_PUBLIC_KEYSTATIC_STORAGE_REPOGitHub only-Repository in owner/repo format
KEYSTATIC_GITHUB_TOKENGitHub only-GitHub personal access token
KEYSTATIC_STORAGE_PROJECTCloud only-Keystatic Cloud project ID
KEYSTATIC_PATH_PREFIXNo-Path to content in monorepos
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATHNo./contentContent directory path
KEYSTATIC_STORAGE_BRANCH_PREFIXNo-Branch prefix for GitHub mode

Troubleshooting

Content not loading in production

Verify GitHub mode is configured:

  • NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=github
  • KEYSTATIC_GITHUB_TOKEN has read access to the repository
  • NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO matches your repository

Admin UI shows authentication error

For GitHub mode, ensure:

  • The Keystatic GitHub App is installed on your repository
  • Your GitHub token has write permissions (for the admin)

Edge runtime errors

Local mode doesn't work on edge. Switch to GitHub or Cloud mode:

  • Set NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=github
  • Configure GitHub token with read access

Next Steps