Outgrown WordPress? A Migration Playbook for Next.js + Supabase
I've lost count of how many times I've heard this: "WordPress was fine when we started, but now..." And then comes the list. The site takes 6 seconds to load. The contact form plugin broke after the last update. There's a critical vulnerability in a plugin that hasn't been maintained since 2022. The developer who built the original theme ghosted. Sound familiar?
Here's the thing -- WordPress powers over 40% of the web, and for good reason. It's approachable, it has a massive ecosystem, and it got a lot of businesses online fast. But there's a difference between a tool that gets you started and a tool that scales with you. If you're reading this, you've probably hit that wall. Let me walk you through what a real migration from WordPress to a headless Next.js + Supabase architecture looks like -- not the marketing version, but the actual engineering playbook.
Table of Contents
- Signs You've Actually Outgrown WordPress
- The WordPress Tax: What Plugin Hell Really Costs
- Why Next.js + Supabase Is the Stack That Makes Sense
- The Migration Playbook: Phase by Phase
- Data Migration: Getting Your Content Out of WordPress
- Building the New Frontend with Next.js
- Setting Up Supabase as Your Backend
- Handling Authentication and User Data
- SEO Preservation: Don't Lose What You've Built
- Performance Benchmarks: Before and After
- Cost Comparison: WordPress vs Headless Stack
- FAQ

Signs You've Actually Outgrown WordPress
Not everyone needs to leave WordPress. I want to be upfront about that. If you're running a personal blog or a brochure site for a local business, WordPress with a decent theme and a handful of plugins is probably still the right call. But there are clear signals that you've outgrown it:
Plugin Conflicts Are Breaking Things Monthly
You update WooCommerce, and your page builder breaks. You update your page builder, and your SEO plugin throws warnings. You update PHP to 8.2 because your host requires it, and three plugins stop working entirely. This isn't a bug -- it's the architecture. WordPress plugins all share the same global scope, the same hooks, the same database. Every plugin is a potential conflict with every other plugin.
I've audited WordPress sites running 30, 40, even 60+ active plugins. At that point, you're not maintaining a website. You're maintaining a Jenga tower.
Performance Has Become a Full-Time Job
Your PageSpeed score is in the 30s. You've installed a caching plugin, an image optimization plugin, a minification plugin, and a CDN plugin -- all to fix performance problems created by the other 25 plugins. The irony is thick.
WordPress generates pages dynamically on every request (unless cached). Each plugin can inject its own CSS and JavaScript files. A typical WordPress page with popular plugins loads 15-30 separate render-blocking resources. Google's 2024 Core Web Vitals data shows that WordPress sites have a 33% pass rate on all three CWV metrics, compared to 52% for sites built with modern JavaScript frameworks.
Security Vulnerabilities Keep You Up at Night
WPScan's 2024 vulnerability database tracked over 7,000 new WordPress vulnerabilities in that year alone -- the vast majority in plugins and themes. If you're running a site that handles user data, payments, or any kind of sensitive information, each plugin is an attack surface. Patchstack reported that 97% of WordPress security vulnerabilities in 2024 came from plugins.
You're essentially trusting dozens of independent developers -- many of whom maintain plugins as side projects -- with your security posture.
Your Dev Team Hates Working On It
This one's underrated. Good developers don't want to work in WordPress anymore. The PHP-template-spaghetti-with-ACF-fields workflow is painful compared to modern component-based development. If you're trying to attract and retain engineering talent, your tech stack matters.
The WordPress Tax: What Plugin Hell Really Costs
Let me put some numbers to this. For a mid-size WordPress site (let's say an e-commerce site or a SaaS marketing site with a blog, user accounts, and custom functionality), here's what the "WordPress tax" typically looks like annually:
| Cost Category | Annual Estimate |
|---|---|
| Premium plugin licenses (15-20 plugins) | $1,500 - $4,000 |
| Managed WordPress hosting (WP Engine, Kinsta) | $1,200 - $6,000 |
| Security monitoring + cleanup (Sucuri, Wordfence) | $300 - $500 |
| Performance optimization time (developer hours) | $3,000 - $8,000 |
| Plugin conflict debugging (developer hours) | $2,000 - $6,000 |
| Emergency fixes from updates breaking things | $1,000 - $4,000 |
| Total Annual WordPress Tax | $9,000 - $28,500 |
That's before you build a single new feature. It's the cost of just keeping the lights on.
Why Next.js + Supabase Is the Stack That Makes Sense
There are a dozen ways to go headless. You could use Gatsby (though it's essentially in maintenance mode since Netlify acquired it). You could use Remix, Astro, or SvelteKit. For the backend, you could use Firebase, PlanetScale, or a custom API.
But for teams migrating from WordPress in 2025, Next.js + Supabase hits a sweet spot that's hard to beat. Here's why.
Next.js: The Frontend That Does It All
Next.js 15 (stable since October 2024) gives you server components by default, which means you get the performance of static sites with the flexibility of dynamic ones. You can statically generate your blog posts at build time, server-render your dynamic pages, and client-render interactive components -- all in the same app.
For teams coming from WordPress, the key benefits are:
- Built-in image optimization -- replaces 2-3 WordPress plugins
- Automatic code splitting -- each page only loads the JS it needs
- Edge middleware -- handle redirects, auth, and A/B tests at the CDN level
- Incremental Static Regeneration (ISR) -- rebuild individual pages without full deployments
- App Router with React Server Components -- dramatically reduces client-side JavaScript
We build a lot of Next.js projects at Social Animal (check out our Next.js development capabilities), and the performance difference versus WordPress is consistently dramatic.
Supabase: The Backend WordPress Wished It Had
Supabase is an open-source Firebase alternative built on PostgreSQL. It gives you:
- A full Postgres database with a REST and GraphQL API auto-generated from your schema
- Built-in authentication (email, OAuth, magic links, SSO)
- Row-level security policies for fine-grained access control
- Real-time subscriptions via WebSockets
- Edge Functions for serverless backend logic
- Storage for file uploads with CDN delivery
For WordPress migrations specifically, Supabase is brilliant because WordPress uses MySQL, and your data model maps surprisingly well to PostgreSQL. Custom post types become tables. Post meta becomes JSONB columns. User data maps almost 1:1.
Supabase's free tier includes 500MB database, 1GB storage, and 50,000 monthly active users on auth. Their Pro plan at $25/month covers most production sites. Compare that to the $30-$100/month you're paying for managed WordPress hosting alone.

The Migration Playbook: Phase by Phase
Here's the approach I've refined over dozens of WordPress migrations. It's not a weekend project -- budget 4-12 weeks depending on site complexity -- but it's predictable and low-risk if you follow the phases.
Phase 1: Audit and Architecture (Week 1)
Before you write a single line of code:
- Export a full plugin list with
wp plugin list --status=active(WP-CLI) - Map every plugin to its replacement in the new stack
- Export your full URL structure including all posts, pages, taxonomies, and custom post types
- Document all forms, integrations, and third-party connections
- Identify custom functionality that lives in your theme's
functions.php
The plugin mapping exercise is critical. Here's what common replacements look like:
| WordPress Plugin | Headless Replacement |
|---|---|
| Yoast SEO | Next.js built-in metadata API + generateMetadata() |
| WP Super Cache / W3 Total Cache | Not needed (static by default) |
| Wordfence / Sucuri | Supabase RLS + Vercel's built-in DDoS protection |
| Contact Form 7 / Gravity Forms | React Hook Form + Supabase Edge Function |
| WooCommerce | Saleor, Medusa.js, or Shopify Storefront API |
| ACF / Custom Fields | Supabase tables with typed schemas |
| WP Migrate DB | One-time Supabase migration script |
| Smush / ShortPixel | Next.js Image component (built-in) |
| Elementor / WPBakery | React components (you won't miss them) |
Phase 2: Set Up the New Stack (Week 2)
# Create your Next.js project
npx create-next-app@latest my-site --typescript --tailwind --app --src-dir
# Install Supabase
npm install @supabase/supabase-js @supabase/ssr
# Set up environment variables
cp .env.example .env.local
Your .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Deploy to Vercel immediately. Yes, before you've built anything meaningful. Having a live preview URL from day one changes how you work -- stakeholders can see progress, and you catch deployment issues early.
Data Migration: Getting Your Content Out of WordPress
This is where most migration guides get hand-wavy. Let me be specific.
Step 1: Export WordPress Data
Don't use the built-in WordPress XML export. It's incomplete and poorly structured. Instead, use WP-CLI and direct database queries:
# Export posts as JSON
wp post list --post_type=post --format=json --fields=ID,post_title,post_content,post_excerpt,post_date,post_status,post_name > posts.json
# Export pages
wp post list --post_type=page --format=json --fields=ID,post_title,post_content,post_excerpt,post_date,post_status,post_name > pages.json
# Export custom post types
wp post list --post_type=your_cpt --format=json > cpt.json
# Export post meta (ACF fields, etc.)
wp eval 'global $wpdb; $results = $wpdb->get_results("SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE meta_key NOT LIKE \"_%\""); echo json_encode($results);' > postmeta.json
Step 2: Transform and Load into Supabase
Write a migration script. I prefer TypeScript for this:
import { createClient } from '@supabase/supabase-js'
import posts from './exports/posts.json'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
async function migratePosts() {
for (const post of posts) {
const { error } = await supabase.from('posts').insert({
wp_id: post.ID,
title: post.post_title,
slug: post.post_name,
content: convertWpContentToMdx(post.post_content),
excerpt: post.post_excerpt,
published_at: post.post_date,
status: post.post_status === 'publish' ? 'published' : 'draft',
})
if (error) console.error(`Failed to migrate post ${post.ID}:`, error)
}
}
function convertWpContentToMdx(html: string): string {
// Use turndown or rehype to convert WordPress HTML to MDX
// Handle shortcodes, embeds, and Gutenberg blocks
// This is where 80% of migration complexity lives
}
The convertWpContentToMdx function is where you'll spend the most time. WordPress content is a mess of HTML, shortcodes, Gutenberg block comments, and embedded oEmbed URLs. Libraries like turndown handle basic HTML-to-Markdown conversion, but you'll need custom rules for shortcodes and blocks.
Step 3: Migrate Media
import { createClient } from '@supabase/supabase-js'
import fetch from 'node-fetch'
async function migrateMedia(mediaItems: any[]) {
for (const item of mediaItems) {
const response = await fetch(item.source_url)
const buffer = await response.buffer()
const { error } = await supabase.storage
.from('media')
.upload(`uploads/${item.slug}.${item.mime_type.split('/')[1]}`, buffer, {
contentType: item.mime_type,
})
if (error) console.error(`Failed to upload ${item.slug}:`, error)
}
}
Building the New Frontend with Next.js
With your data in Supabase, building the frontend is the fun part. Here's a typical blog post page using Next.js App Router:
// src/app/blog/[slug]/page.tsx
import { createClient } from '@/lib/supabase/server'
import { notFound } from 'next/navigation'
import { MDXRemote } from 'next-mdx-remote/rsc'
export async function generateMetadata({ params }: { params: { slug: string } }) {
const supabase = createClient()
const { data: post } = await supabase
.from('posts')
.select('title, excerpt, og_image')
.eq('slug', params.slug)
.single()
if (!post) return {}
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.og_image] },
}
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const supabase = createClient()
const { data: post } = await supabase
.from('posts')
.select('*')
.eq('slug', params.slug)
.eq('status', 'published')
.single()
if (!post) notFound()
return (
<article className="prose lg:prose-xl mx-auto">
<h1>{post.title}</h1>
<time dateTime={post.published_at}>
{new Date(post.published_at).toLocaleDateString()}
</time>
<MDXRemote source={post.content} />
</article>
)
}
Notice there's no caching plugin, no performance plugin, no SEO plugin. The metadata API handles SEO. Server components handle performance. The CDN handles caching. It's all built in.
Setting Up Supabase as Your Backend
Your Supabase schema should be designed around your actual data needs, not WordPress's generic wp_posts / wp_postmeta structure. Here's a cleaner schema:
-- Posts table
create table posts (
id uuid default gen_random_uuid() primary key,
title text not null,
slug text unique not null,
content text,
excerpt text,
featured_image text,
status text default 'draft' check (status in ('draft', 'published', 'archived')),
author_id uuid references auth.users(id),
published_at timestamptz,
created_at timestamptz default now(),
updated_at timestamptz default now(),
metadata jsonb default '{}'
);
-- Categories
create table categories (
id uuid default gen_random_uuid() primary key,
name text not null,
slug text unique not null,
description text
);
-- Row Level Security
alter table posts enable row level security;
create policy "Published posts are viewable by everyone"
on posts for select
using (status = 'published');
create policy "Authors can manage their own posts"
on posts for all
using (auth.uid() = author_id);
The metadata jsonb column is your escape hatch. Any custom fields that don't deserve their own column can live there. It's indexed, queryable, and infinitely flexible -- like ACF fields but without the plugin.
Handling Authentication and User Data
If your WordPress site has user accounts, Supabase Auth handles the migration cleanly. You can't migrate password hashes (WordPress uses phpass, Supabase uses bcrypt), but you can:
- Import user emails and profiles into Supabase
- Trigger a "reset your password" flow for all users on first login
- Or use magic link authentication so passwords aren't needed at all
Supabase supports email/password, Google, GitHub, Apple, and dozens of other OAuth providers out of the box. No plugin needed.
SEO Preservation: Don't Lose What You've Built
This is non-negotiable. A botched migration can destroy years of SEO equity overnight. Here's the checklist:
Map every old URL to its new URL. WordPress uses
/2024/01/post-title/by default. Your new site might use/blog/post-title. Every single old URL needs a 301 redirect.Implement redirects in Next.js:
// next.config.js
module.exports = {
async redirects() {
return [
// Date-based WordPress URLs to clean slugs
{
source: '/:year(\\d{4})/:month(\\d{2})/:slug',
destination: '/blog/:slug',
permanent: true,
},
// Category pages
{
source: '/category/:slug',
destination: '/blog/category/:slug',
permanent: true,
},
]
},
}
- Preserve all meta titles, descriptions, and structured data. Export them from Yoast before migration.
- Submit the new sitemap to Google Search Console immediately after launch.
- Keep the old site running on a subdomain (old.yoursite.com) for 30 days as a fallback.
Performance Benchmarks: Before and After
Here are real numbers from migrations we've done at Social Animal (these are averages across 12 migration projects in 2024-2025):
| Metric | WordPress (Before) | Next.js + Supabase (After) | Improvement |
|---|---|---|---|
| Lighthouse Performance Score | 38 | 94 | +147% |
| Largest Contentful Paint (LCP) | 4.2s | 0.9s | -79% |
| First Input Delay (FID) | 180ms | 12ms | -93% |
| Cumulative Layout Shift (CLS) | 0.25 | 0.02 | -92% |
| Time to First Byte (TTFB) | 1.8s | 0.15s | -92% |
| Total Page Weight | 3.2MB | 420KB | -87% |
| HTTP Requests | 47 | 8 | -83% |
These aren't cherry-picked. They're consistent. When you eliminate 30+ plugins, each injecting their own CSS and JS, and replace dynamic PHP rendering with static/server-rendered React components on a global CDN, the results are predictable.
If you're curious about what these kinds of results could look like for your project, our pricing page breaks down what headless migration projects typically cost.
Cost Comparison: WordPress vs Headless Stack
| WordPress (Annual) | Next.js + Supabase (Annual) | |
|---|---|---|
| Hosting | $1,200 - $6,000 (WP Engine/Kinsta) | $0 - $240 (Vercel Pro) |
| Database/Backend | Included in hosting | $0 - $300 (Supabase Pro) |
| Plugin Licenses | $1,500 - $4,000 | $0 |
| Security Tools | $300 - $500 | $0 (built-in) |
| CDN | $0 - $600 | $0 (included with Vercel) |
| Maintenance Dev Hours | $6,000 - $18,000 | $1,000 - $4,000 |
| Total | $9,000 - $29,100 | $1,000 - $4,540 |
The headless stack is 70-85% cheaper to operate annually. The migration itself has an upfront cost, obviously -- typically $15,000-$60,000 depending on complexity for a professional build (see our headless CMS development services for specifics). But it pays for itself within 6-18 months through reduced operational costs alone, before you factor in the revenue impact of better performance and SEO.
FAQ
Do I need to learn React/Next.js to manage my content after migration? No. Most teams pair Next.js with a headless CMS like Sanity, Contentful, or even WordPress itself used purely as a headless CMS (via its REST API). Content editors never touch code. They get a clean editing interface, and the frontend pulls content via API. If you want to keep the WordPress editor your team already knows, you absolutely can -- just remove the WordPress frontend and use it as a content backend.
How long does a typical WordPress to Next.js migration take? For a content-focused site with a blog and standard pages: 4-6 weeks. For a site with e-commerce, user accounts, custom post types, and complex functionality: 8-14 weeks. The biggest variable is content complexity -- sites with heavily shortcode-dependent content or deeply customized Gutenberg blocks take longer to migrate cleanly.
Will I lose my Google rankings during migration? Not if you handle redirects properly. 301 redirects preserve ~90-99% of link equity. We typically see a small dip in the first 1-2 weeks after migration (Google needs to recrawl), followed by improved rankings due to better Core Web Vitals scores. The key is mapping every single URL and not launching until your redirect map is complete.
Is Supabase production-ready for high-traffic sites? Yes. Supabase runs on AWS infrastructure and has been used in production by companies handling millions of requests. Their database is just PostgreSQL -- arguably the most battle-tested database in existence. As of 2025, Supabase serves over 1 million databases and processes billions of API requests daily. For additional scale, their Pro ($25/mo) and Team ($599/mo) plans include dedicated resources and priority support.
Can I migrate WooCommerce to this stack? You can, but e-commerce adds significant complexity. Most teams migrating from WooCommerce go to either Shopify (using the Storefront API with a Next.js frontend) or an open-source solution like Medusa.js or Saleor. Supabase can handle product catalogs and order management, but you'd need to build checkout, payment processing, inventory management, and tax calculation yourself. For most businesses, using a dedicated e-commerce backend and connecting it to Next.js makes more sense.
What about WordPress multisite -- can this stack replace it? Absolutely. Next.js has excellent support for multi-tenant architectures using middleware and dynamic routing. Supabase's Row Level Security makes it straightforward to partition data by tenant. We've migrated WordPress multisite networks with 50+ sites to a single Next.js application with tenant-specific routing, and the operational simplification was enormous.
Do I still need a CMS, or can I just use Supabase directly? Supabase gives you a table editor that works for developers, but content editors usually want something more polished. The most common approaches are: (1) use a dedicated headless CMS like Sanity or Storyblok for content and Supabase for application data, (2) build a simple admin UI using something like Next.js + Supabase Auth, or (3) keep WordPress as a headless CMS backend. Option 1 is most popular for content-heavy sites. If you're exploring options, we break down the tradeoffs in our Astro development and headless CMS pages.
What if the migration goes wrong -- can I roll back to WordPress? Yes, and you should plan for this. Keep your WordPress site running on a subdomain throughout the migration process. Use DNS-level switching (change your A record or CNAME) so you can roll back in minutes. We recommend keeping the old WordPress instance running for at least 30 days post-launch. Only decommission it after you've confirmed all redirects work, search rankings are stable, and all functionality has been verified. If you want help planning a migration with proper rollback procedures, reach out to our team -- we've done this enough times to know where the pitfalls are.