Skip to main content

Payload Starter vs Sanity Starter vs Strapi Starter

·StarterPick Team
payload-cmssanitystrapicmssaas-boilerplatenext-js2026

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

Find CMS-powered starters and boilerplates at StarterPick.

Comments