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:
| Mode | Storage | Best For | Edge Compatible |
|---|---|---|---|
local | Local filesystem | Development, solo projects | No |
github | GitHub repository | Production, team collaboration | Yes |
cloud | Keystatic Cloud | Managed hosting | Yes |
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=keystaticNEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=localKEYSTATIC_PATH_PREFIX=apps/webNEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./contentContent structure:
apps/web/content/├── posts/ # Blog posts├── documentation/ # Docs (supports nesting)└── changelog/ # Release notesLimitations: 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
# .envCMS_CLIENT=keystaticNEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=githubNEXT_PUBLIC_KEYSTATIC_STORAGE_REPO=your-org/your-repoKEYSTATIC_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxKEYSTATIC_PATH_PREFIX=apps/webNEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./content2. Create a GitHub Token
- Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Create a new token with:
- Repository access: Select your content repository
- Permissions: Contents (Read-only for production, Read and write for admin UI)
- Copy the token to
KEYSTATIC_GITHUB_TOKEN
For read-only access (recommended for production):
KEYSTATIC_GITHUB_TOKEN=github_pat_xxxxxxxxxxxx3. 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/webNEXT_PUBLIC_KEYSTATIC_CONTENT_PATH=./contentKeystatic Cloud
Keystatic Cloud is a managed service that handles GitHub authentication and provides a hosted admin UI.
# .envCMS_CLIENT=keystaticNEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=cloudKEYSTATIC_STORAGE_PROJECT=your-project-idGet 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 keystaticThis creates a route at /keystatic where you can create and edit content.
Protect the admin in production
By default, the Keystatic admin is only available in development. For production, add authentication:
app/keystatic/layout.tsx
import { redirect } from 'next/navigation';import { isSuperAdmin } from '@kit/admin';export default async function KeystaticLayout({ children,}: { children: React.ReactNode;}) { const isAdmin = await isSuperAdmin(); if (!isAdmin) { redirect('/'); } return children;}GitHub Mode Admin Setup
GitHub mode requires a GitHub App for the admin UI to authenticate and commit changes.
- Install the Keystatic GitHub App on your repository
- 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-15status: publishedcategories: - tutorialstags: - getting-startedimage: /images/posts/getting-started.webp---Content here...Documentation
Hierarchical docs with ordering and collapsible sections:
---title: "Authentication"label: "Auth" # Short label for navigationdescription: "How authentication works"order: 1status: publishedcollapsible: truecollapsed: 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-10status: 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:
# HeadingParagraph with **bold** and *italic*.- List item- Another item```codeCode block### ImagesImages are stored in `public/site/images/` and referenced with the public path:```markdownCustom 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 fileconst 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
| Variable | Required | Default | Description |
|---|---|---|---|
CMS_CLIENT | No | keystatic | CMS provider |
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND | No | local | Storage mode: local, github, cloud |
NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO | GitHub only | - | Repository in owner/repo format |
KEYSTATIC_GITHUB_TOKEN | GitHub only | - | GitHub personal access token |
KEYSTATIC_STORAGE_PROJECT | Cloud only | - | Keystatic Cloud project ID |
KEYSTATIC_PATH_PREFIX | No | - | Path to content in monorepos |
NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH | No | ./content | Content directory path |
KEYSTATIC_STORAGE_BRANCH_PREFIX | No | - | Branch prefix for GitHub mode |
Troubleshooting
Content not loading in production
Verify GitHub mode is configured:
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=githubKEYSTATIC_GITHUB_TOKENhas read access to the repositoryNEXT_PUBLIC_KEYSTATIC_STORAGE_REPOmatches 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
- CMS API Reference: Learn the full API for fetching content
- CMS Overview: Compare CMS providers
- Keystatic Documentation: Official Keystatic docs