Your Next.js app is already a CMS.
Easy CMS runs inside your Next.js or Nuxt app. Define content in TypeScript, and you get an admin at /admin, a typed API and REST, on one deployment and your own database.
npm create easy-cms@latest

const cms = await getEasyCMS(config)
const { docs } = await cms.find('posts')
// docs: Post[], typed from your config- Next.js 15+
- Nuxt
- PostgreSQL
- SQLite · Turso
- PGlite
- S3 · R2 · MinIO
- Vercel · Netlify
No second server to run, pay for or keep in sync.
A typical headless CMS is another service with its own database and API keys. Easy CMS is an npm package your app mounts, so content ships with the code that renders it.
Two apps, two databases
- Schema lives in a web UI, not in your repo
- Types generated in a separate step
- A network hop on every page render
One app, one database
ecms_ tables- Content model in
easy-cms.config.ts, reviewed in PRs - Types come straight from the config, no build step
- Deploys wherever your app already runs
Three files and your app has a CMS.
Describe collections and fields in TypeScript. The admin forms, validation, access rules and the typed API are built from that one file.
import { defineConfig } from '@easy-cms/core'
import { postgres } from '@easy-cms/db-postgres'
import { seoPlugin } from '@easy-cms/plugin-seo'
export default defineConfig({
secret: process.env.EASY_CMS_SECRET!,
db: postgres({ url: process.env.DATABASE_URL! }),
localization: { locales: ['en', 'th'], defaultLocale: 'en' },
collections: [
{
slug: 'posts',
drafts: true, versions: true, schedule: true,
useAsTitle: 'title',
fields: [
{ name: 'title', type: 'text', required: true, localized: true },
{ name: 'slug', type: 'slug', from: 'title' },
{ name: 'body', type: 'richText', localized: true },
],
},
],
plugins: [seoPlugin({ collections: ['posts'] })],
})// app/admin/[[...path]]/route.ts
import { createAdminRouteHandlers } from '@easy-cms/next'
import config from '@/easy-cms.config'
export const { GET, HEAD } = createAdminRouteHandlers(config)
// app/api/cms/[[...path]]/route.ts
import { createRouteHandlers } from '@easy-cms/next'
import config from '@/easy-cms.config'
export const { GET, HEAD, POST, PATCH, PUT, DELETE, OPTIONS } =
createRouteHandlers(config)
// next.config.ts
import { withEasyCMS } from '@easy-cms/next/config'
export default withEasyCMS({ /* your Next config */ })import { getEasyCMS } from '@easy-cms/next'
import config from '@/easy-cms.config'
export const dynamic = 'force-dynamic'
export default async function Posts() {
const cms = await getEasyCMS(config)
const { docs } = await cms.find('posts', { limit: 10 })
return (
<ul>
{docs.map((post) => (
// post.title: string, typed from your config
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}What editors expect, what developers can review.
Drafts and version history
Edit a published post without touching the live page. Compare versions and restore any of them.
Live preview
Your real page sits next to the form and updates as editors type, before anything is published.
Localized content
One value per language, with translation status in every list. The admin speaks English and Thai.
Roles and single sign-on
Tick what each role may do in the admin, down to fields. Sign in with Google, Microsoft or GitHub.
A media library with folders
Folders, many files at once, private files with signed links, and storage on S3, R2 or Vercel Blob.
Audit log and backups
A signed record of who changed what, and database backups you start or schedule from the admin.
Scheduled publishing
Publish or unpublish at a set time. Due jobs run every minute on the server you already have.
SEO out of the box
Sitemaps, hreflang, JSON-LD and llms.txt from the content you already have.
Typed API, REST and GraphQL
A typed Local API in your server code, REST for any client, and GraphQL and MCP as plugins.
Fast at a million documents.
Every night a load test runs Easy CMS against 1,000,000 posts. The times are p95: 95% of requests finish faster.
- posts in one collection
- 1,000,000
- a post by id or slug
- 27ms
- the next page by cursor, however deep
- 37ms
- Thai text search with a trigram index
- 79ms
Easy CMS 1.15 · median of 3 runs · Postgres 17 · GitHub runner, 4 cores · 20 requests at once · 28 Oct 2026
Not everything is fast yet: a page picked by number deep in the list takes seconds (use cursor instead), and so do queries inside blocks. The full results list them.
Add a feature with one line in plugins.
Each plugin adds its own collections, admin screens and API routes, and ships its migration. Write your own with the same hooks.
@easy-cms/plugin-seoMeta fields, sitemaps, hreflang and JSON-LD.
seo@easy-cms/plugin-form-builderEditors build forms; submissions are validated, stored and exported as CSV.
forms@easy-cms/plugin-redirects301 to 308 redirects, created automatically when a page moves.
seo@easy-cms/plugin-nested-docsPages inside pages, with paths and breadcrumbs per language.
pages@easy-cms/plugin-multi-tenantSeveral sites or clients in one CMS, each with its own members and content.
0.44@easy-cms/plugin-ecommerceProducts, carts and checkout with Stripe, PromptPay or bank transfer.
0.46@easy-cms/plugin-graphqlA GraphQL API with the same access rules as REST.
0.43@easy-cms/plugin-mcpAn MCP server so AI assistants can work with your content.
aiAn admin your editors will actually like.
Generated from your config, in light and dark, English and Thai. Nothing to design or host.


Latest releases and guides
Easy CMS 1.13: built for a million documents
Six releases since 1.7 for collections of a million posts: cursors, Thai search with trigram indexes, HTTP caching, an outbox and search adapters.
Run Easy CMS on several servers with Docker and Postgres
Two Easy CMS instances behind a load balancer, on one Postgres and one bucket, with Docker Compose. Stop either one and the site keeps answering.
Easy CMS 1.7: ready to run on several servers
Three releases since 1.4 make Easy CMS ready to run on several servers: Postgres poolers, JSON logs with request ids, health checks, and an official Docker image.
Stable since 1.0
- Breaking changes come only in a major release, each with an upgrade guide.
- A renamed API keeps working until the next major, with a warning.
- Every change is tested against databases made by older releases, on SQLite and Postgres.
- Security fixes reach the previous major for six months after the next one is out.
Ship content with the code that renders it.
Scaffold a Next.js or Nuxt project with Easy CMS in about a minute. It is MIT licensed and runs on your own infrastructure.