What is Statix CMS?
Statix CMS is a free, lightweight content management system that uses GitHub as its content database, Cloudflare R2 for media storage, and Turso for user/auth data.
Unlike traditional CMS packages, Statix CMS is not installed as a dependency. When you run npx create-statix-cms, you get a complete, standalone Next.js application that you fully own and control.
How It Works
Statix CMS uses a unique architecture that separates concerns across three services.
GitHub
Content stored as JSON files. Every save creates a Git commit with full version history and metadata.
Cloudflare R2
S3-compatible media and image storage served via public URL. Organize into folders, track references.
Turso
Serverless SQLite for users, sessions, audit logs, and invitations. Edge-compatible session validation.
Authentication — Better Auth
- Email OTP via Resend
- GitHub OAuth (optional)
- Google OAuth (optional)
Quick Start
Create a new project
Scaffold your CMS with a single command.
npx create-statix-cms my-cms
cd my-cmsSet up required services
You need GitHub (content storage), Turso (user database), and Resend (email OTP). All have free tiers. Optional: Cloudflare R2, GitHub OAuth, Google OAuth.
Configure environment
Fill in .env with your credentials.
GITHUB_TOKEN=ghp_xxxxxxxxxxxx
GITHUB_OWNER=your-username
GITHUB_REPO=my-cms-content
BETTER_AUTH_SECRET=your-secret-here
BETTER_AUTH_URL=http://localhost:3000
TURSO_DATABASE_URL=libsql://your-db.turso.io
TURSO_AUTH_TOKEN=your-turso-token
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM_EMAIL=cms@yourdomain.comStart the dev server
npm run dev
# or
bun run devCreate your admin account
Sign in with OTP, then promote yourself to Owner.
# Add to .env:
INITIAL_ADMIN_EMAIL=your@email.com
# Then run:
npm run seed:adminConfigure your content
Edit src/statix.config.ts to define your collections and fields.
Prerequisites
GitHub Personal Access Token
RequiredGo to github.com/settings/tokens, generate a classic token with repo scope. Create a separate repository for content.
Turso Database
RequiredSign up at turso.tech. Create a database and get the connection URL and auth token.
turso db create my-cms
turso db show my-cms --url
turso db tokens create my-cmsResend (Email OTP)
RequiredSign up at resend.com, create an API key, and verify your sender domain.
Cloudflare R2
OptionalCreate a bucket, generate API token with Read & Write permissions, enable public access.
GitHub OAuth
OptionalCreate OAuth App at github.com/settings/developers. Callback: {BETTER_AUTH_URL}/api/auth/callback/github
Google OAuth
OptionalCreate OAuth 2.0 Client at Google Cloud Console. Redirect URI: {BETTER_AUTH_URL}/api/auth/callback/google
Environment Variables
All variables are validated at startup using Zod. The app will not start if required variables are missing.
| Variable | Required | Description |
|---|---|---|
| GITHUB_TOKEN | required | GitHub Personal Access Token with repo scope |
| GITHUB_OWNER | required | GitHub username or organization |
| GITHUB_REPO | required | Repository name for content storage |
| GITHUB_BRANCH | optional | Branch to use (default: main) |
| BETTER_AUTH_SECRET | required | Auth secret — generate with openssl rand -base64 32 |
| BETTER_AUTH_URL | required | Full site URL (e.g., http://localhost:3000) |
| TURSO_DATABASE_URL | required | Turso connection URL (libsql://...turso.io) |
| TURSO_AUTH_TOKEN | required | Turso authentication token |
| RESEND_API_KEY | required | Resend API key for sending OTP emails |
| RESEND_FROM_EMAIL | required | Verified sender email address |
| GITHUB_CLIENT_ID | optional | GitHub OAuth app Client ID |
| GITHUB_CLIENT_SECRET | optional | GitHub OAuth app Client Secret |
| GOOGLE_CLIENT_ID | optional | Google OAuth Client ID |
| GOOGLE_CLIENT_SECRET | optional | Google OAuth Client Secret |
| R2_ACCOUNT_ID | optional | Cloudflare account ID |
| R2_ACCESS_KEY_ID | optional | R2 API token access key |
| R2_SECRET_ACCESS_KEY | optional | R2 API token secret key |
| R2_BUCKET_NAME | optional | R2 bucket name |
| NEXT_PUBLIC_MEDIA_BASE_URL | optional | Public URL for R2 bucket |
| INITIAL_ADMIN_EMAIL | optional | Email to promote to Owner via seed:admin |
Configuration
All content modeling is done in src/statix.config.ts — the single source of truth for your CMS structure.
Base Configuration
import { StatixConfig } from "@/statix/types";
export const statixConfig: StatixConfig = {
github: {
owner: process.env.GITHUB_OWNER || "",
repo: process.env.GITHUB_REPO || "",
branch: process.env.GITHUB_BRANCH || "main",
},
mediaFolder: "uploads",
i18n: {
locales: ["en", "tr"],
defaultLocale: "en",
},
roles: [],
collections: [],
};Singleton Example
Single pages with unique content (e.g., Home, About, Contact):
{
slug: "home",
label: "Home Page",
type: "singleton",
path: "content/home",
icon: "Home",
fields: [
{ name: "title", label: "Page Title", type: "text", required: true, localized: true },
{ name: "heroImage", label: "Hero Image", type: "image" },
{ name: "ctaText", label: "Button Text", type: "text" },
],
}Collection Example
Repeatable items (e.g., Blog Posts, Team Members):
{
slug: "blog",
label: "Blog Posts",
path: "content/blog",
icon: "FileText",
titleField: "title",
fields: [
{ name: "title", label: "Title", type: "text", required: true, localized: true },
{ name: "date", label: "Publish Date", type: "date", required: true },
{ name: "featuredImage", label: "Featured Image", type: "image" },
{ name: "content", label: "Content", type: "blocks", localized: true },
],
}i18n Configuration
i18n: {
locales: ["en", "tr", "de", "fr"],
defaultLocale: "en",
}Content Modeling
12 field types available for building your content schema.
text
placeholder, localized
Single-line text input
textarea
rows, placeholder, localized
Multi-line text input
richtext
placeholder, localized
WYSIWYG rich text editor
image
Image picker from media library
file
File upload
number
Numeric input
select
options: [{label, value}]
Dropdown selection
date
Date picker
checkbox
Checkbox toggle
switch
Toggle switch
list
fields: Field[]
Repeatable group of fields
blocks
blocks: [{type, label, fields}]
Drag-and-drop content blocks
Roles & Permissions
Three system roles with granular permissions, plus support for custom roles.
| Role | Manage Users | View Monitor | Manage Media | Manage Trash | Content |
|---|---|---|---|---|---|
| Owner | ✓ | ✓ | ✓ | ✓ | Full |
| Admin | ✓ | ✓ | ✓ | ✓ | Full |
| Editor | ✗ | ✗ | ✗ | ✗ | View, Create, Edit |
Custom Roles
Define custom roles in statix.config.ts with per-collection permissions: canView, canCreate, canEdit, canDelete, canPublish.
Admin Panel
Dashboard
/admin
Collection statistics, localization progress, recent activity feed.
Collections
/admin/[slug]
List view with search, pagination, and status indicators.
Content Editor
/admin/[slug]/[id]
Field-based form, block editor, locale switching, draft/publish.
Media Library
/admin/media
Upload, browse, organize, search, move, and delete media files.
Users
/admin/users
Create/invite users, assign roles, ban/unban, per-user audit logs.
Trash
/admin/trash
Browse soft-deleted content and media. Restore or permanently delete.
Monitor
/admin/monitor
Audit logs, activity charts, commit timeline, GitHub API rate limit.
API Reference
All API routes (except /api/auth and /api/media/serve) require authentication. Protected by CSRF validation and rate limiting (100 req/min per IP).
Authentication
| Method | Endpoint | Description |
|---|---|---|
| * | /api/auth/[...all] | Better Auth handler (login, logout, OTP, OAuth) |
Content
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/content/[collectionSlug]/[id] | Get content item |
| POST | /api/content/[collectionSlug]/[id] | Create or update content |
| DELETE | /api/delete/[collectionSlug]/[id] | Soft-delete content |
| GET | /api/collections/[slug] | Get collection items list |
Media
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/upload | Upload a file |
| GET | /api/media/list | List media files |
| POST | /api/media/delete | Soft-delete media |
| POST | /api/media/move | Move media between folders |
| GET | /api/media/references | Find content using a media file |
| GET | /api/media/stats | Storage statistics |
| GET | /api/media/serve/[...path] | Serve media files (public) |
Trash
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/trash/list | List soft-deleted items |
| POST | /api/trash/restore | Restore item from trash |
| DELETE | /api/trash/delete | Permanently delete item |
| GET | /api/trash/media/[filename] | Get trashed media metadata |
Admin
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/users | List users |
| POST | /api/admin/users | Create or update user |
| POST | /api/admin/users/[id]/avatar | Upload user avatar |
| GET | /api/admin/activity | Activity feed |
| GET | /api/admin/audit | Audit logs |
Tech Stack
| Category | Technology | Version |
|---|---|---|
| Framework | Next.js (App Router) | 16.x |
| UI | React | 19.x |
| Styling | Tailwind CSS | 4.x |
| Language | TypeScript | 5.x |
| Auth | Better Auth | 1.x |
| Database ORM | Drizzle ORM | 0.45.x |
| Database | Turso (libsql) | — |
| GitHub API | Octokit | 5.x |
| Object Storage | AWS SDK S3 (Cloudflare R2) | 3.x |
| State | Zustand | 5.x |
| Data Fetching | TanStack React Query | 5.x |
| Forms | React Hook Form | 7.x |
| Validation | Zod | 4.x |
| Rich Text | ProseKit | 0.19.x |
| Drag & Drop | dnd-kit | 6.x |
| Charts | Recharts | 2.x |
| Resend | 6.x | |
| Toasts | Sonner | 2.x |
| Icons | Tabler Icons React | 3.x |
| Components | shadcn/ui + Base UI | — |
Deployment
Deploy to Vercel (recommended) or any platform that supports Next.js.
Push your project to GitHub
Import the repository in Vercel Dashboard
Set all environment variables
Deploy
After Deployment
- Update BETTER_AUTH_URL to your production domain
- Update OAuth callback URLs to production domain
- Generate a new BETTER_AUTH_SECRET for production
Other Platforms
Pre-launch Checklist
- All required environment variables are set
- BETTER_AUTH_SECRET generated fresh for production
- BETTER_AUTH_URL set to production domain
- OAuth callback URLs updated to production domain
- Turso database created for production
- R2 bucket configured with public access (if using media)
- First admin user seeded via npm run seed:admin
Project Structure
src/
├── app/
│ ├── (statix)/
│ │ ├── admin/
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── [collectionSlug]/ # Collection list + editor
│ │ │ ├── media/ # Media library
│ │ │ ├── users/ # User management
│ │ │ ├── trash/ # Trash management
│ │ │ └── monitor/ # Audit & monitoring
│ │ ├── auth/ # Sign-in, invite acceptance
│ │ └── api/ # API routes (20 endpoints)
│ ├── layout.tsx # Root layout
│ └── page.tsx # Public home page
│
├── statix/
│ ├── components/
│ │ ├── ui/ # shadcn/ui base components
│ │ ├── editor/ # Rich text & block editor
│ │ ├── fields/ # Field type renderers
│ │ ├── collections/ # Collection list views
│ │ ├── dashboard/ # Dashboard widgets
│ │ ├── media/ # Media library UI
│ │ ├── users/ # User management UI
│ │ ├── trash/ # Trash UI
│ │ ├── monitor/ # Monitoring charts
│ │ ├── activity/ # Activity feed
│ │ ├── layout/ # Navigation, breadcrumbs
│ │ ├── shared/ # Reusable components
│ │ └── skeletons/ # Loading placeholders
│ ├── hooks/ # 18 custom React hooks
│ ├── lib/ # Core utilities
│ ├── stores/ # Zustand state stores
│ ├── types/ # TypeScript definitions
│ ├── db/ # Drizzle ORM schema
│ └── content/ # ui.json (translations)
│
├── statix.config.ts # Content model configuration
├── middleware.ts # CSRF, rate limiting, auth
└── lib/
└── utils.ts # General utilitiesapp/(statix)/Route group for all CMS routesstatix/components/All React components, organized by featurestatix/hooks/18 custom hooks for data fetching, state, etc.statix/lib/Core utilities (GitHub API, R2, auth, rate limiting)statix.config.tsSingle source of truth for content modeling