Skip to main content

Payload vs Sanity vs Strapi CMS Starters 2026

·StarterPick Team
Share:

TL;DR

Payload 3.0 is the most compelling CMS for Next.js developers in 2026 — it runs inside your Next.js app with no separate server. Sanity is the best cloud-hosted CMS for content teams (real-time collaboration, asset management, GROQ queries). Strapi is the self-hosted workhorse with REST and GraphQL APIs. For developer-run SaaS products: Payload is the new default. For content-heavy sites where non-technical editors work: Sanity. For REST/GraphQL API teams: Strapi.

Key Takeaways

  • Payload 3.0: Next.js App Router native, TypeScript-first, admin panel = your Next.js app, ~50K downloads/week
  • Sanity: Cloud-hosted GROQ queries, real-time collaboration, @sanity/next-loader, 250K downloads/week
  • Strapi 5: Self-hosted, REST + GraphQL auto-generated, drag-and-drop content types, 400K downloads/week
  • Database: Payload → your Postgres/MongoDB; Sanity → Sanity cloud; Strapi → SQLite/Postgres/MySQL
  • Auth: Payload has built-in auth; Sanity uses Sanity user management; Strapi has built-in auth
  • For SaaS: Payload (admin panel doubles as CMS = one app to deploy)

Payload 3.0: CMS Inside Next.js

Payload 3.0's paradigm shift: the CMS and your app are the same Next.js project. The admin panel is a Next.js route (/admin), not a separate service.

# New project with Payload:
npx create-payload-app@latest
# Or add to existing Next.js:
npm install payload @payloadcms/next @payloadcms/richtext-lexical
// payload.config.ts — your CMS schema in TypeScript:
import { buildConfig } from 'payload';
import { postgresAdapter } from '@payloadcms/db-postgres';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { nodemailerAdapter } from '@payloadcms/email-nodemailer';

export default buildConfig({
  secret: process.env.PAYLOAD_SECRET!,
  
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URL },
  }),
  
  email: nodemailerAdapter({
    defaultFromAddress: 'hello@myapp.com',
    defaultFromName: 'My App',
    transportOptions: { host: 'smtp.resend.com', auth: { pass: process.env.RESEND_API_KEY } },
  }),

  admin: {
    user: 'users',  // 'users' collection handles admin auth too
    importMap: { baseDir: path.resolve(dirname) },
  },
  
  collections: [
    // Users with auth (SaaS-ready):
    {
      slug: 'users',
      auth: {
        verify: true,
        forgotPassword: { generateEmailHTML: ({ token }) => `<a href="/reset?token=${token}">Reset</a>` },
      },
      fields: [
        { name: 'name', type: 'text' },
        { name: 'plan', type: 'select', options: ['free', 'pro', 'team'], defaultValue: 'free' },
        { name: 'stripeCustomerId', type: 'text', admin: { hidden: true } },
      ],
    },
    
    // Blog posts:
    {
      slug: 'posts',
      admin: { useAsTitle: 'title' },
      access: {
        read: () => true,                 // Public
        create: ({ req }) => !!req.user,  // Auth required
        update: ({ req, id }) => req.user?.id === id,
        delete: ({ req }) => req.user?.roles?.includes('admin'),
      },
      fields: [
        { name: 'title', type: 'text', required: true },
        { name: 'slug', type: 'text', unique: true },
        { name: 'content', type: 'richText', editor: lexicalEditor() },
        { name: 'author', type: 'relationship', relationTo: 'users' },
        { name: 'publishedAt', type: 'date' },
        { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
      ],
      versions: { drafts: true },  // Draft/publish workflow
    },
  ],

  plugins: [],
});
// Querying in Next.js Server Component:
import { getPayload } from 'payload';
import config from '@payload-config';

export async function getPosts() {
  const payload = await getPayload({ config });
  
  const posts = await payload.find({
    collection: 'posts',
    where: { status: { equals: 'published' } },
    sort: '-publishedAt',
    limit: 10,
    depth: 1,  // Resolve relationships
  });
  
  return posts.docs;
}

// In page.tsx:
export default async function BlogPage() {
  const posts = await getPosts();  // Direct DB query, no HTTP overhead
  return <PostList posts={posts} />;
}

Sanity: Cloud-First with GROQ

npm install next-sanity @sanity/image-url @sanity/cli
npx sanity@latest init
// sanity.config.ts:
import { defineConfig } from 'sanity';
import { structureTool } from 'sanity/structure';
import { visionTool } from '@sanity/vision';
import { schemaTypes } from './schemas';

export default defineConfig({
  name: 'default',
  title: 'My SaaS Blog',
  
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: 'production',
  
  plugins: [structureTool(), visionTool()],
  schema: { types: schemaTypes },
});
// schemas/post.ts — Sanity schema:
import { defineField, defineType } from 'sanity';

export const postType = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({ name: 'title', type: 'string', validation: (r) => r.required() }),
    defineField({ name: 'slug', type: 'slug', options: { source: 'title' } }),
    defineField({ name: 'body', type: 'array', of: [{ type: 'block' }] }),
    defineField({ name: 'publishedAt', type: 'datetime' }),
    defineField({
      name: 'author',
      type: 'reference',
      to: [{ type: 'author' }],
    }),
  ],
  preview: {
    select: { title: 'title', author: 'author.name', media: 'mainImage' },
  },
});
// Querying with GROQ (Sanity's query language):
import { createClient } from 'next-sanity';

const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: 'production',
  apiVersion: '2024-01-01',
  useCdn: true,  // CDN for reads, real-time for writes
});

// GROQ query:
const posts = await client.fetch(`
  *[_type == "post" && !(_id in path("drafts.**"))] | order(publishedAt desc) {
    _id,
    title,
    "slug": slug.current,
    publishedAt,
    "author": author->{ name, "avatar": image.asset->url },
    "estimatedReadingTime": round(length(pt::text(body)) / 5 / 180)
  }[0...10]
`);

Strapi 5: Self-Hosted API CMS

npx create-strapi-app@latest my-project
# Prompts: Database (SQLite for dev, Postgres for prod)
// Content type builder: drag-and-drop UI at /admin
// Or via code (src/api/blog-post/content-types/blog-post/schema.json):
{
  "kind": "collectionType",
  "collectionName": "blog_posts",
  "info": { "singularName": "blog-post", "pluralName": "blog-posts" },
  "attributes": {
    "title": { "type": "string", "required": true },
    "slug": { "type": "uid", "targetField": "title" },
    "content": { "type": "richtext" },
    "publishedAt": { "type": "datetime" },
    "author": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.user" }
  }
}
// Fetching from Strapi in Next.js:
const posts = await fetch(
  `${process.env.STRAPI_URL}/api/blog-posts?populate=author&sort=publishedAt:desc&pagination[limit]=10`,
  { headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` } }
).then(r => r.json());

Comparison Table

Payload 3.0SanityStrapi 5
HostingSelf-hosted (in your app)CloudSelf-hosted
DatabasePostgres/MongoDB (your DB)Sanity cloudSQLite/Postgres/MySQL
Next.js integrationNative (same process)HTTP + CDNHTTP API
Auth✅ Built-in users/authSanity users✅ Users + permissions
Real-timeVia webhooks✅ Real-time collabLimited
GROQ/filteringPayload query APIGROQ (powerful)REST + filters
Asset managementS3/localSanity Asset PipelineUpload providers
SaaS use case✅ Admin panel = CMSPartialPartial
PriceFree (self-hosted)Free tier, $99+/moFree (self-hosted)
CommunityGrowingLargeLarge

Decision Guide

Choose Payload if:
  → Building a Next.js SaaS (admin = CMS, one deployment)
  → Want TypeScript-first schema definition
  → Need auth + CMS in one system
  → Self-hosted is required

Choose Sanity if:
  → Content team needs real-time collaboration
  → Rich media/asset management is important
  → Content-heavy site (not SaaS admin)
  → Okay with cloud dependency (SaaS pricing)

Choose Strapi if:
  → Need REST or GraphQL API auto-generated
  → Non-technical editors comfortable with Strapi UI
  → Multi-framework consumers (mobile + web)
  → Self-hosted with traditional SQL database

What "CMS as Your App" Means for SaaS Products

The architectural shift Payload 3.0 represents deserves more explanation. Traditional headless CMS tools (Sanity, Strapi) are separate services: you run a CMS server at one URL and your application server at another. Payload 3.0 eliminates this split — the admin panel lives at /admin inside your Next.js application, the collections are defined in TypeScript files in your repo, and the CMS uses the same database your application uses. There is no separate CMS deployment, no CORS configuration, no separate API key rotation cycle.

For SaaS products where the admin panel serves both as an internal dashboard and as a content management interface, this is a meaningful simplification. Your team doesn't manage two deployments, two sets of environment variables, or two monitoring setups. The Payload admin panel shows you application data (users, subscriptions, orders) and content (blog posts, product descriptions, documentation) side by side, because they're in the same PostgreSQL database.

The tradeoff is that Payload requires developers to define content schemas in TypeScript code. Non-technical content editors can use the admin UI once the schema is defined, but schema changes require a developer to modify code, commit, and deploy. Sanity's schema definition approach is similar, but Strapi's drag-and-drop content type builder lets non-technical users create and modify content types without touching code.

Sanity's Strengths: Real-Time Collaboration and GROQ

Sanity's differentiated features are its real-time collaborative editing and GROQ — a query language purpose-built for document trees. GROQ lets you write queries that traverse relationships and compute derived values in a single expression, without multiple round trips. For content-rich sites with complex relationships (authors with articles, articles with categories, categories with featured content), GROQ queries can replace multiple REST API calls with one concise expression.

The collaborative editing experience in Sanity Studio — multiple editors seeing each other's cursors and edits in real time — is genuinely impressive and not matched by Payload or Strapi. For media organizations, documentation teams, or content-heavy products with multiple editors working simultaneously, this feature alone can justify Sanity's pricing.

Sanity's asset management pipeline handles image transformations at the CDN level: you store one original and request different sizes, crops, and formats via URL parameters. Sanity delivers the transformed version from cache. This eliminates the need for Cloudinary or imgix for most content use cases.

The cost structure changes materially once you exceed the free tier. Sanity charges per dataset, per seat, and per API request at higher tiers. Teams doing high-volume content operations should run the pricing calculator carefully before committing.

Strapi in Production: REST, GraphQL, and Scale

Strapi's position in 2026 is as the REST/GraphQL API generator. Strapi 5 generates both REST endpoints and a full GraphQL schema from the content types you define in its drag-and-drop builder. For teams that need to feed content to multiple consumers — a Next.js web app, an iOS app, an Android app, an Astro marketing site — Strapi's auto-generated multi-consumer API is compelling.

Strapi v5 introduced significant architectural changes from v4, including a new Document Service API that replaces the direct ORM usage pattern. Migration from v4 to v5 requires meaningful refactoring of any custom code. Teams running Strapi v4 in production should evaluate the migration cost against the v5 improvements before upgrading.

Self-hosting Strapi adds operational complexity compared to Sanity Cloud or Supabase's managed Postgres. You're responsible for database backups, security updates, scaling, and uptime. The create-strapi-app scaffold configures SQLite by default for development, but production deployments need PostgreSQL. Getting from local development to a production Strapi deployment with proper media uploads (S3), PostgreSQL, and a reverse proxy takes a few hours of DevOps work.

Choosing Based on Team Composition

Team composition matters as much as feature requirements when choosing a CMS. A team of full-stack developers who are comfortable with TypeScript and value code-first schema definition should default to Payload — it gives them the most control with the least deployment overhead. A team with dedicated content editors who need to modify content types without deploying code should lean toward Strapi for its drag-and-drop builder. A team at a media company or content-heavy product where real-time collaboration and rich asset management are daily requirements should consider Sanity despite its higher ongoing cost.

The single-deployment benefit of Payload is particularly valuable for small teams and solo developers who don't want to operate multiple services. The Payload admin panel covers what your team needs to manage both the application and the content, without the additional infrastructure overhead.

Pricing Reality Check

Pricing is where the three options diverge most sharply for growing SaaS products. Payload running on your own infrastructure has zero CMS licensing cost — you pay for database and hosting, both of which you'd have anyway. Strapi self-hosted is also free. Sanity's free tier covers 3 datasets, 20 seats, and 200,000 API reads per month, which is generous for small teams but can tighten for content-heavy applications or large editorial teams.

The Sanity growth tier starts at $99/month and scales with usage. For a media-heavy product doing millions of content API reads per month, Sanity costs can become a meaningful line item. The value justification depends on whether the asset management pipeline and collaborative editing features would otherwise require separate Cloudinary, imgix, or collaborative editing tooling — in which case Sanity's all-in price may be competitive.

Strapi Enterprise pricing adds SSO, priority support, and audit logs on top of self-hosted Strapi. For enterprise SaaS products with compliance requirements, this tier is relevant. For most SaaS products, community Strapi is sufficient and the free self-hosted option provides full functionality.

Migration Paths Between CMS Options

One consideration teams underestimate is the migration path if they outgrow or change their CMS choice. Content stored in Sanity's cloud is exportable via their API and CLI (sanity dataset export), but the export format is NDJSON with Sanity-specific portable text blocks that require transformation to fit any other schema. Migrating away from Sanity is a defined effort, typically requiring custom transformation scripts.

Strapi content is stored in your own database (PostgreSQL or SQLite), making it technically straightforward to export. The challenge is transforming Strapi's internal data structure (with its metadata layers) into a clean format for another system. Strapi provides export tools, but migration to a radically different CMS architecture requires custom work.

Payload content, being in your own PostgreSQL database with a schema you define, is the most portable. The database is yours, the schema is readable TypeScript, and any query tool or ORM can access the data. Moving away from Payload means replacing the admin panel layer, not the data layer.

Performance and Caching

Content APIs are frequently in the critical path for page rendering. How each CMS handles caching and delivery affects your application's performance characteristics.

Sanity's CDN (Content Delivery Network) caches API responses globally. When useCdn: true in your client configuration, reads come from the CDN rather than Sanity's origin. This is fast and appropriate for content that changes infrequently. For content that updates frequently (live data, frequently-edited articles), Sanity's real-time API bypasses the CDN and hits the origin directly.

Payload, running in the same Next.js application, queries the database directly — no HTTP round trip for content fetches. In a Next.js Server Component, await payload.find({...}) is a direct Postgres query. Combined with Next.js's unstable_cache or the cache option on fetch requests, content can be served from the Next.js data cache for subsequent requests without touching the database. This architecture can achieve lower latency than an external CMS API when properly cached.

Strapi requires HTTP API calls from your Next.js application, similar to Sanity. The performance depends on network proximity (hosting Strapi in the same region as your Next.js deployment helps), and caching at the application level (storing Strapi responses in Redis or using Next.js's unstable_cache).

The content type extensibility also differs across the three systems. Payload's field types are defined in TypeScript and can include custom validation logic, conditional field visibility, and complex relationship rules expressed as code. This code-first approach makes complex content models precise but requires a developer to modify them. Sanity's studio is extensible via React components — you can build custom field types and previews that non-technical editors see without understanding the implementation. Strapi's plugin system allows custom fields and functionality without touching core Strapi code, which matters for teams running a self-hosted Strapi version they need to keep updatable.

For teams deciding between a full CMS integration and a simpler boilerplate: the Payload CMS starter review covers the specific Payload SaaS template in detail. The best SaaS boilerplates with built-in blog compares boilerplates that include content management without requiring a separate CMS. For the database layer specifically, see the Supabase vs Firebase vs Appwrite comparison.

The Decision Framework in Plain Terms

After all the feature comparisons, the decision between Payload, Sanity, and Strapi reduces to three questions.

First: who will manage the content schema going forward? If schema changes require a developer to commit code and deploy, all three options have the same requirement — but Strapi's visual content type builder means a non-technical team member can make schema changes without a code deploy, at the cost of less precise control over the data model. If your team has no developer bandwidth available for schema changes after launch, Strapi's visual builder is a meaningful operational advantage.

Second: does your content require real-time collaborative editing? If multiple editors will work simultaneously on the same content — a news organization, a content agency, a product with dedicated editors across time zones — Sanity's collaborative editing is the only production-ready option among the three. Payload and Strapi don't offer collaborative editing.

Third: how important is deployment simplicity? Payload deploys with your Next.js application — no additional service, no additional domain, no additional certificate. Sanity deploys its admin to Sanity Cloud — you don't manage it, but you do have a dependency on Sanity's availability. Strapi self-hosted requires its own server, deployment pipeline, and operational attention. If you're a solo developer or a small team where operational overhead is a genuine constraint, Payload's single-deployment model is the most pragmatic choice.

For teams who find the CMS decision secondary to the broader SaaS boilerplate choice, best SaaS boilerplates 2026 covers which full-stack starters include content management without requiring a separate CMS service.

Find CMS-powered starters and boilerplates at StarterPick.

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.