I've set up Strapi for at least a dozen client projects over the past few years. It's a great CMS to get started with -- friendly admin panel, decent plugin ecosystem, solid docs. But somewhere around the 18-month mark, almost every team I've worked with hits a wall. The migrations get painful, the customizations feel hacky, and your TypeScript-loving frontend devs start grumbling about the developer experience. If that sounds familiar, you're not alone. Let me walk you through the signs that you've outgrown Strapi, and more importantly, what to build instead.

When You've Outgrown Strapi CMS: Building with Payload + Supabase

Table of Contents

The Honeymoon Phase: Why Strapi Works Early On

Let's give credit where it's due. Strapi has 65K+ GitHub stars for a reason. When you're spinning up a new content-driven project, the Content-Type Builder is genuinely useful. You click around in a GUI, define some fields, and boom -- you've got REST and GraphQL APIs. Non-technical content editors can log into the admin panel and start publishing within an hour.

For early-stage startups, marketing sites, and blogs, Strapi is a perfectly reasonable choice. It supports PostgreSQL, MySQL, MariaDB, and SQLite. The plugin marketplace has over 100 community extensions. And the community edition is free under MIT license.

I've recommended Strapi plenty of times. I'm not here to trash it. I'm here to talk about what happens next.

Six Signs You've Outgrown Strapi

These are the patterns I see repeatedly. If three or more of these resonate, it's time to start planning your migration.

1. Migrations Make You Nervous

The v4 to v5 upgrade is the one that broke a lot of teams. Strapi documented 50+ breaking changes. In practice, I've seen migrations take 40+ hours of developer time for medium-complexity projects. That's not a minor version bump -- that's a rewrite of your content layer.

If you're dreading the next major version because you know it'll break half your custom controllers, you've outgrown the tool.

2. Your Developers Are Fighting the Framework

Strapi's Content-Type Builder is great for non-technical users. For developers who live in TypeScript and want code-first schemas, it's a bottleneck. The GUI generates files you're not supposed to edit. The lifecycle hooks feel bolted on. TypeScript support was added late in v4 and still isn't native -- it's more of an afterthought.

When your team starts building workarounds for the framework instead of building features with it, that's a clear signal.

3. Performance Is Becoming a Problem

Strapi's cold starts typically range from 500ms to 2000ms. Global latency sits between 200-500ms without aggressive caching. For a blog, that's fine. For an e-commerce platform serving users across multiple regions, it's not.

I had a client running a multi-language product catalog on Strapi. Once they crossed ~50,000 content entries with deep relationships, API response times crept past the 2-second mark. No amount of indexing fixed it.

4. You Need Features That Are Locked Behind Paid Tiers

Strapi's Growth plan runs $45/month and includes 3 seats (additional seats at $15/seat each). That unlocks AI features, live preview, and content history. Enterprise pricing is custom.

The issue isn't the price -- it's the principle. Features like live preview and content versioning feel table-stakes for a CMS in 2025. Having them gated behind a subscription on an open-source tool rubs developer teams the wrong way.

5. Your Stack Has Evolved Past Strapi's Architecture

If you've gone all-in on Next.js, Vercel, and TypeScript, Strapi starts to feel like an awkward appendage. It runs as a separate Node.js service. That means separate deployments, separate monitoring, separate scaling concerns. Your frontend team deploys to Vercel in seconds while your CMS needs its own infrastructure.

6. Custom Extensions Feel Like Surgery

Want to add conditional field logic? Nested repeatable blocks with validation? Custom admin panel components? In Strapi, each of these is possible but painful. The admin panel is a React app, but extending it means navigating a proprietary extension system that doesn't feel like normal React development.

When You've Outgrown Strapi CMS: Building with Payload + Supabase - architecture

Understanding Your Options: Payload vs Supabase vs Staying Put

Before we go further, let's be clear about what these tools actually are, because I see them conflated constantly.

Tool What It Actually Is Best For Not Great For
Strapi Open-source headless CMS with admin GUI Content editors, quick API generation Code-first teams, high-perf apps
Payload CMS Code-first CMS framework that runs inside Next.js TypeScript devs, custom content apps Non-technical solo operators
Supabase Open-source backend platform (DB, auth, storage, realtime) Application data, auth, file storage Content editing workflows
Directus SQL-first headless CMS with auto-generated APIs Teams with existing databases Deep Next.js integration
Sanity Managed headless CMS with real-time collaboration Editorial teams, structured content Budget-conscious self-hosters

Payload and Supabase aren't competitors -- they're complementary. Payload handles your content modeling, admin UI, and content APIs. Supabase handles your database hosting, authentication, file storage, and real-time subscriptions. Together, they replace Strapi and then some.

Why Payload CMS Is the Natural Next Step

Payload has been making serious waves through 2025 and into 2026. MG Software called it a "market disruptor" for TypeScript/Next.js integration, and I think that's accurate.

Here's the fundamental architectural difference: Payload runs inside your Next.js application. Not alongside it. Not as a separate service. Inside it. Your CMS and your frontend share the same deployment, the same TypeScript types, the same build process.

Code-First Schema Definition

Instead of clicking through a GUI to create content types, you write TypeScript:

import { CollectionConfig } from 'payload';

export const Products: CollectionConfig = {
  slug: 'products',
  admin: {
    useAsTitle: 'name',
    defaultColumns: ['name', 'price', 'status'],
  },
  access: {
    read: () => true,
    create: ({ req: { user } }) => user?.role === 'admin',
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
    },
    {
      name: 'price',
      type: 'number',
      min: 0,
    },
    {
      name: 'description',
      type: 'richText',
    },
    {
      name: 'category',
      type: 'relationship',
      relationTo: 'categories',
      hasMany: false,
    },
    {
      name: 'variants',
      type: 'array',
      fields: [
        { name: 'sku', type: 'text', required: true },
        { name: 'color', type: 'select', options: ['red', 'blue', 'green'] },
        {
          name: 'stock',
          type: 'number',
          admin: {
            condition: (data, siblingData) => siblingData?.sku !== undefined,
          },
        },
      ],
    },
  ],
  hooks: {
    afterChange: [
      async ({ doc, operation }) => {
        if (operation === 'update' && doc.stock === 0) {
          // Trigger restock notification
        }
      },
    ],
  },
};

That's a real content type with conditional field logic, nested arrays, relationship fields, role-based access control, and lifecycle hooks. All in one file. All type-safe. All version-controlled in Git.

Try doing that in Strapi without touching three different files and the admin extension system.

Three API Layers

Payload auto-generates REST and GraphQL APIs from your schema. But the killer feature is the Local API -- direct database queries from your server-side code with zero HTTP overhead:

// Inside a Next.js Server Component
import { getPayload } from 'payload';
import config from '@payload-config';

export default async function ProductPage({ params }) {
  const payload = await getPayload({ config });
  
  const product = await payload.findByID({
    collection: 'products',
    id: params.id,
    depth: 2, // Populate relationships 2 levels deep
  });
  
  return <ProductDetail product={product} />;
}

No fetch calls. No API routes. No serialization overhead. This is why Payload benchmarks at roughly 7x faster than Strapi for content retrieval in comparable setups.

Where Supabase Fits In (And Where It Doesn't)

Supabase isn't a CMS. I want to be really clear about that. It's a backend platform built on PostgreSQL. But it solves several problems that Strapi handles poorly and Payload doesn't handle at all.

What Supabase Brings to the Stack

  • Managed PostgreSQL: Payload's PostgreSQL adapter connects directly to a Supabase database. You get connection pooling, automatic backups, and a dashboard for direct SQL queries. The Pro plan starts at $25/month and scales with usage.
  • Authentication: Supabase Auth supports email/password, magic links, OAuth providers, and row-level security. If your app has user-facing features beyond the CMS admin, this is huge.
  • File Storage: S3-compatible object storage with CDN. Payload can handle media uploads, but Supabase Storage handles application files, user uploads, and anything outside the CMS context.
  • Real-time Subscriptions: PostgreSQL's LISTEN/NOTIFY wrapped in a clean WebSocket API. Pair this with Payload's live preview for real-time content editing experiences.
  • Edge Functions: Deno-based serverless functions for webhook handlers, background jobs, and integrations.

What Supabase Doesn't Do

It doesn't give you an admin panel for content editors. It doesn't generate content APIs with rich-text fields and media management. It doesn't handle content localization or version history. That's Payload's job.

The Payload + Supabase Stack: Architecture Deep Dive

Here's how I set this up for teams migrating off Strapi. This is the architecture we typically recommend at Social Animal for Next.js development projects where content management is a core requirement.

Project Structure

├── src/
│   ├── app/                    # Next.js App Router
│   │   ├── (frontend)/         # Public-facing pages
│   │   └── (payload)/          # CMS admin routes (auto-generated)
│   ├── collections/            # Payload collection configs
│   │   ├── Posts.ts
│   │   ├── Products.ts
│   │   └── Users.ts
│   ├── globals/                # Payload global configs
│   │   └── SiteSettings.ts
│   ├── lib/
│   │   └── supabase.ts         # Supabase client initialization
│   └── payload.config.ts       # Main Payload config
├── supabase/
│   └── migrations/             # Supabase DB migrations
├── .env.local
└── package.json

Connecting Payload to Supabase PostgreSQL

// payload.config.ts
import { buildConfig } from 'payload';
import { postgresAdapter } from '@payloadcms/db-postgres';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { Posts } from './collections/Posts';
import { Products } from './collections/Products';

export default buildConfig({
  db: postgresAdapter({
    pool: {
      connectionString: process.env.SUPABASE_DB_URL,
      // Use Supabase's connection pooler for serverless
      max: 10,
    },
  }),
  editor: lexicalEditor(),
  collections: [Posts, Products],
  secret: process.env.PAYLOAD_SECRET,
  typescript: {
    outputFile: path.resolve(__dirname, 'payload-types.ts'),
  },
});

One important note: use Supabase's connection pooler URL (port 6543), not the direct connection URL, when deploying to serverless environments like Vercel. Direct connections work fine for local development.

Adding Supabase Auth for Application Users

Payload handles CMS admin authentication. Supabase handles everything else:

// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

// In a Server Component or API route
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export async function getServerSupabase() {
  const cookieStore = await cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          );
        },
      },
    }
  );
}

This separation is clean: Payload owns content. Supabase owns application data and user identity. They share the same PostgreSQL instance but stay in their respective lanes.

Migration Strategy: Moving Off Strapi Without Losing Your Mind

I won't sugarcoat this -- migration takes work. But it's dramatically less painful than a Strapi v4-to-v5 upgrade, because you're moving to a system that respects your existing mental models.

Step-by-Step Approach

  1. Audit your Strapi content types. Export them as JSON from the Content-Type Builder. Map each one to a Payload collection config.
  2. Set up Payload in a new Next.js project. Run npx create-payload-app@latest and select the PostgreSQL adapter.
  3. Point Payload at your Supabase database. Create a new Supabase project, grab the connection string, configure the adapter.
  4. Recreate schemas in TypeScript. This is the bulk of the work. Each Strapi content type becomes a Payload collection. Fields map almost 1:1 -- text, number, richtext, relation, media.
  5. Export data from Strapi. Use pg_dump if you were on PostgreSQL, or write a script against the Strapi REST API to pull all content.
  6. Import into Payload. Use Payload's Local API in a seed script to bulk-create content.
  7. Update your frontend. Swap Strapi API calls for Payload Local API calls (in Server Components) or REST/GraphQL calls (in client components).
  8. Deploy. Since Payload runs inside Next.js, you deploy one application instead of two.

For complex migrations, our headless CMS development team can help plan the transition. We've done this enough times to know where the landmines are.

Performance Benchmarks and Real-World Numbers

Here are the numbers that matter, gathered from published benchmarks and our own testing in 2025:

Metric Strapi v5 Payload 3.x Payload + Supabase
Cold start time 500-2000ms 100-300ms 150-350ms
Simple query (single item) 45-80ms 8-15ms 10-20ms
Complex query (populated, 3 levels) 200-500ms 30-80ms 40-100ms
Admin panel load 2-4s 1-2s 1-2s
Build time (500 pages, ISR) N/A (separate) 45-90s 50-100s
Global latency (no CDN) 200-500ms 50-150ms 60-160ms

The Supabase connection pooler adds a small overhead compared to a direct PostgreSQL connection, typically 5-15ms. That's negligible in practice.

One caveat: Payload's populated field queries (deep relationship resolution) have been flagged as slower than raw database queries -- roughly 15x slower than direct Mongoose/Drizzle calls according to GitHub issue #11325. For read-heavy pages, I recommend using Next.js ISR or caching populated results rather than hitting the database on every request.

When This Stack Isn't the Right Call

I'd be doing you a disservice if I didn't mention the scenarios where Payload + Supabase is overkill or simply wrong.

  • Your team doesn't write TypeScript. Payload is code-first. If your content team manages everything and you don't have developers maintaining the CMS, Strapi's GUI is genuinely better. Or look at Sanity or Contentful.
  • You need a massive plugin ecosystem today. Payload's ecosystem is growing but it's smaller than Strapi's 100+ extensions. If you need a specific integration that exists as a Strapi plugin but not for Payload, factor in the build time.
  • You're building a simple blog or marketing site. Strapi works fine for this. So does Astro with a headless CMS. Don't over-engineer it.
  • You need edge-first performance. If sub-50ms global latency is a hard requirement, look at SonicJS on Cloudflare Workers. Payload runs on Node.js -- it's fast, but it's not edge-native.

Want to talk through whether this migration makes sense for your specific project? Reach out to us -- we'll give you an honest assessment, not a sales pitch.

FAQ

Is Payload CMS really free?

Yes. Payload is MIT-licensed and free to self-host with no feature restrictions. There's an optional Payload Cloud hosting service for teams that don't want to manage infrastructure, but all CMS features -- live preview, access control, localization, versioning -- are available in the open-source version. Compare that to Strapi, which gates live preview and content history behind the $45/month Growth plan.

Can I use Supabase as a CMS by itself?

Technically, you could build a content management interface on top of Supabase using its auto-generated APIs and Row Level Security. But you'd be rebuilding what Payload gives you out of the box: an admin panel, rich text editing, media management, content versioning, and access control. Supabase is a backend platform, not a CMS. Use it for what it's good at -- database hosting, auth, storage -- and let Payload handle the content layer.

How long does it take to migrate from Strapi to Payload?

For a typical project with 10-20 content types and a few thousand entries, expect 2-4 weeks of developer time. The schema recreation is the biggest piece -- translating Strapi content types to Payload TypeScript configs. Data migration is usually a day or two with a good script. Frontend updates depend on how tightly coupled your code is to Strapi's API response format. Teams that used a data-access layer or abstraction will have an easier time.

Does Payload work with databases other than PostgreSQL?

Payload supports both MongoDB and PostgreSQL through official database adapters. For the Supabase stack described in this article, you'd use the PostgreSQL adapter. If you're coming from a MongoDB background, Payload's MongoDB adapter is actually more mature since MongoDB was Payload's original database. Both are production-ready in 2025.

What about Directus as an alternative to Strapi?

Directus is a solid option, especially if you have an existing SQL database you want to expose through a CMS interface. It auto-generates an admin panel and APIs from your database schema, which is clever. The main drawback compared to Payload is the lack of deep Next.js integration -- Directus runs as a separate service, similar to Strapi. If your team isn't on the Next.js/Vercel stack, Directus deserves serious consideration.

Can I deploy Payload + Supabase on Vercel?

Absolutely -- this is actually the ideal deployment target. Since Payload runs inside your Next.js app, deploying to Vercel is as simple as pushing to your Git repo. Supabase runs as a managed service, so there's nothing to deploy on that side. Your total infrastructure becomes: Vercel for compute and CDN, Supabase for database and auth. Two services, one deployment pipeline. We frequently set this up for clients through our Next.js development practice.

Is the Payload admin panel customizable?

Very. The admin UI is built with React and supports custom components, custom views, and custom fields. You can add dashboard widgets, create custom navigation items, and override any default UI element. Since Payload 3.x, admin customization uses React Server Components, which means your custom admin code can fetch data server-side without API calls. It's a different world from Strapi's admin extension system, which requires ejecting and patching.

What does this stack cost to run in production?

For a small-to-medium project: Vercel Pro at $20/month + Supabase Pro at $25/month = $45/month total. That gives you serverless compute, a managed PostgreSQL database with 8GB storage, 250MB file storage, 50,000 monthly active auth users, and automatic backups. Scale up from there as needed. Compare that to self-hosting Strapi on a VPS ($20-50/month) plus managing your own database, or Strapi Cloud's Growth plan at $45/month with limited seats. The Payload + Supabase combo is cost-competitive and dramatically easier to operate. For detailed project estimates, check our pricing page.