Back

Why We Built Our Own Headless CMS

Published by January 21, 2025 · Reading time 6 minutes · Created by Cod'Hash Team

The Reality: Existing CMS Solutions Don't Meet Real Needs

After years working with different CMS platforms for our clients — WordPress, Strapi, Contentful, Sanity — one conclusion became clear: no existing solution truly addresses the needs of modern projects.

This isn't a blanket criticism. These are concrete observations from real production projects.

The Problems We Encountered

WordPress: A relational database used as a file system. Every plugin adds dozens of tables. Simple multilingual content becomes a nightmare with WPML, Polylang, or TranslatePress. Result? Catastrophic performance and accumulating technical debt.

Strapi: Promising on paper, but reality differs. Managing complex relationships quickly becomes hell. The permission system lacks granularity. Worst of all, the v3 to v4 migration broke so many projects that teams chose to rewrite everything from scratch.

Contentful & Sanity: Excellent for structured content, but pricing explodes with real traffic. $500/month to manage 10 company blogs? You're paying for features you don't need.

The real problem? These CMS platforms aren't designed for the specific needs of modern agencies and SaaS products.

What We Actually Needed

Analyzing our last 15 client projects, clear patterns emerged:

Simple and Predictable REST API

No complex GraphQL with 50 optional fields. Our frontend teams want:

  • GET /api/v1/blog/articles → article list
  • GET /api/v1/blog/articles/[slug] → specific article
  • POST /api/v1/blog/articles → create article

That's it. Clear endpoints, API key authentication, and it works. No 200-page documentation to read.

Native Multilingual, Not a Plugin

When clients ask for a FR/EN/DE blog, we don't want to:

  • Install a plugin adding 12 database tables
  • Manage colliding slugs
  • Debug why /fr/article returns the English version

Our solution? Each article has native translations. One slug per language. SEO metadata per language. All in a clean, predictable data structure.

// Actual API structure
{
  "id": "article-123",
  "status": "PUBLISHED",
  "translations": [
    {
      "language": "en",
      "slug": "why-headless-cms",
      "title": "Why a Headless CMS?",
      "seoTitle": "Headless CMS: Our Custom Solution",
      "published": true
    },
    {
      "language": "fr",
      "slug": "pourquoi-cms-headless",
      "title": "Pourquoi un CMS headless ?",
      "seoTitle": "CMS Headless : Notre Solution sur-mesure",
      "published": true
    }
  ]
}

Multi-Organization Without Friction

Clients often want to manage multiple blogs. But with traditional CMS:

  • One installation per blog (WordPress)
  • One project per environment (Contentful at $500/month × N)
  • Data migrations for every new client

Our approach? Multi-tenant SaaS. Each organization has its blog, API keys, team, billing. One deployment. One database. One codebase.

Granular Permissions

A modern CMS must handle teams. Here's what we implemented:

  • OWNER: Complete management (articles, members, billing)
  • EDITOR: Create, edit, publish articles
  • WRITER: Create and edit, but no publishing

Each member can have API key permissions:

  • blog:read — read-only access
  • blog:write — create/edit
  • blog:publish — publish articles
  • blog:manage — full management

No rigid predefined roles. Composable permissions.

Integrated SEO, Not an Afterthought

Generate an XML sitemap? It's an endpoint:

GET /api/v1/blog/sitemap.xml

RSS feed? Same logic:

GET /api/v1/blog/rss.xml

Open Graph metadata, estimated reading time, optimized images, canonical URLs — all handled natively. No need to install Yoast or equivalent.

Our Technical Stack

We didn't start from scratch. We chose proven technologies:

Next.js 15 + React 19 + TypeScript

  • Server Components by default (less client-side JavaScript)
  • App Router for clear structure
  • Turbopack for fast builds

PostgreSQL + Prisma

  • Solid relational database
  • Versioned migrations
  • Modular schema (one .prisma file per feature)

better-auth

  • Native organizations
  • Magic links for authentication
  • Stripe integration for billing

Validation and Types

  • Zod for data validation
  • react-hook-form for forms
  • End-to-end type safety

Documented REST API

  • Consistent endpoints (/api/v1/blog/*)
  • Automatic validation with Zod
  • Structured error handling

Features That Make the Difference

Integrated Analytics

Every article view is tracked with:

  • Language accessed
  • Country of origin (no invasive tracking)
  • Real-time trending articles

Endpoint:

GET /api/v1/blog/analytics

Native Full-Text Search

Search across title, excerpt, and content. Filter by category, tag, status. Sort by date or popularity.

GET /api/v1/blog/search?q=headless&category=dev&sort=popular

Upload and Media Management

UploadThing for uploads. Track dimensions, size, usage. Alt text for accessibility.

API Keys with Scopes

Create API keys with precise permissions:

{
  "name": "Frontend Production",
  "permissions": ["blog:read"],
  "expiresAt": "2026-01-21"
}

Concrete Results

After 6 months of development and 3 months in production:

Performance

  • REST API: <100ms average response time
  • Sitemap generation: <200ms for 1000 articles
  • Full-text search: <150ms

Development

  • New client = 5 minutes (organization creation)
  • Frontend integration = 1 day (vs 1 week with Strapi)
  • Content migration = automated script

Costs

  • One Vercel instance = all clients
  • Managed PostgreSQL = $25/month
  • No per-project or per-user fees

What We Learned

What Works

Headless is the right approach. Our clients want Next.js, Nuxt, Astro. They don't want WordPress themes.

API simplicity pays off. No complex GraphQL. Clear REST endpoints. 2-page documentation.

Native multilingual is essential. It's not a plugin. It's core to the data model.

What Needs Improvement

Media management. Currently functional but basic. We're planning:

  • Automatic image compression
  • Responsive srcset generation
  • S3 storage with CDN

Webhooks. Currently requires polling the API. We're adding:

  • Webhooks on article creation/modification
  • Automatic cache invalidation
  • Integration with Vercel/Netlify for rebuilds

Content editor. Tiptap works, but we want:

  • Reusable blocks (code, quotes, callouts)
  • Real-time preview
  • Internal link suggestions

Conclusion: A Solution Built for Our Needs

We didn't create this CMS for technical ego. We built it because no existing solution met our real needs:

  • Simple and predictable REST APIs
  • Native multilingual without plugins
  • Multi-tenant SaaS with integrated billing
  • Granular role-based permissions
  • Integrated SEO and analytics
  • Optimal performance (Next.js 15, React Server Components)

This CMS isn't for everyone. If you need a simple 10-page static website, WordPress will work fine.

But if you're building:

  • A SaaS with multilingual blogs per client
  • A content platform with distributed teams
  • A technical documentation hub
  • A programmatic publishing system via API

Then you'll encounter the same limitations we did. And you'll understand why we made this choice.


Want to discuss your project or learn more? Contact us.