Command Palette

Search for a command to run...

2.2k
Blog

Payload + Next.js: The Content Layer I Pair with Medusa Now

Commerce engines are great at products and terrible at pages. Why Payload CMS became my default content layer next to Medusa, how the two share one Postgres instance, and where the editor experience earns its keep.

Every commerce build hits the same wall. Medusa handles products, carts, and orders beautifully, and then the client asks where they edit the homepage hero. Or the brand story page. Or the holiday campaign landing page that marketing wants to ship without filing a ticket.

For years my answer was Contentful or some headless SaaS, and it worked, but it always felt like renting a second brain. Separate dashboard, separate auth, separate bill, separate API with its own rate limits. When I did the Medusa v1 to v2 migration I wrote about last year, the client wanted content editing in the same breath, and that project is where Payload became my default. I've used it on three builds since. This post is the why and the how.

Why Payload Specifically

Payload is a CMS you install as a dependency, not a service you sign up for. Since version 3 it runs natively inside a Next.js app: the admin panel is a route in your project, the collections are TypeScript files in your repo, and the whole thing deploys with the storefront as one unit.

That one property changes the economics of a client project:

  • One deployment. The storefront, the admin panel, and the content API ship together on Vercel. No second pipeline.
  • One database. Payload speaks Postgres. Medusa speaks Postgres. They live in the same instance, separate schemas, one backup story.
  • Config is code. Collections are version-controlled TypeScript. When a client asks for a new field on the careers page, that's a pull request, not clicking through a dashboard and hoping staging matches production.
  • The types are real. Payload generates TypeScript types from your collections. The frontend imports them. Rename a field and the build breaks where it should.

The honest tradeoff: you're now operating a CMS instead of paying someone else to. Upgrades are yours, the admin panel's uptime is your uptime. For a SaaS-averse client with a competent dev partner, that trade is good. For a marketing team with no engineering support, Contentful still earns its fee.

The Shape of the Integration

The architecture on these builds is boring in the best way. Next.js App Router project, three concerns:

  1. Medusa owns anything transactional: products, variants, prices, inventory, carts, orders. It runs as its own service.
  2. Payload owns anything an editor touches: pages, heroes, navigation, FAQs, the blog, campaign banners, SEO metadata.
  3. The storefront stitches them. A product page pulls structured commerce data from Medusa and editorial blocks from Payload in the same server component.
// app/products/[handle]/page.tsx
export default async function ProductPage({ params }) {
  const { handle } = await params
 
  const [product, editorial] = await Promise.all([
    medusa.store.product.list({ handle }),
    payload.find({
      collection: "product-stories",
      where: { productHandle: { equals: handle } },
    }),
  ])
 
  return (
    <>
      <ProductDetails product={product} />
      {editorial.docs[0] && <StoryBlocks blocks={editorial.docs[0].blocks} />}
    </>
  )
}

The join key between the two systems is the product handle. Payload has a product-stories collection where editors attach rich content to any product: a lookbook, care instructions, a founder note. If no story exists, the page renders fine without it. Editors enrich what matters and ignore the rest, which is how editorial energy actually gets spent.

Blocks Are the Feature

The thing that sold the client wasn't the architecture. It was the blocks field.

Payload lets you define a set of typed content blocks (hero, two-column feature, quote, product grid, FAQ accordion) and editors compose pages by stacking them. Each block is a React component on the frontend with a matching TypeScript type. The marketing lead builds a campaign page on Thursday afternoon by stacking blocks, previews it, publishes it. Nobody from engineering is involved, and nothing she can build will break the design system, because she can only assemble pieces we shipped.

// collections/Pages.ts
{
  name: "layout",
  type: "blocks",
  blocks: [Hero, FeatureGrid, Quote, ProductGrid, FAQAccordion],
}

On the migration project this replaced a folder of pages that previously required a developer for every copy change. The ticket volume for "please change this banner" went to zero in the first month.

Sharing Postgres Without Sharing Trouble

Both systems on one Postgres instance sounds risky, and done carelessly it would be. The rules that kept it clean:

  • Separate schemas (payload.* and medusa.*), separate database users, no cross-schema foreign keys. The product handle join happens in application code, never in SQL.
  • Payload's tables are tiny compared to commerce data. Editorial content is thousands of rows, not millions. It adds nothing meaningful to backup size or restore time.
  • Draft and publish state lives entirely in Payload. Medusa never knows or cares that a holiday banner is scheduled for Friday.

The one real incident in six months: a Payload migration locked a table at the same moment a Medusa product sync was writing, and the sync slowed for about a minute. We moved Payload migrations to a deploy step that runs outside business hours. That was the whole fix.

Where It Bit Me

Live preview took tuning. Payload's preview wants to render your actual frontend with draft content. With ISR caching in front of everything, editors saw stale pages and assumed publishing was broken. The fix was a preview route that sets draftMode() and bypasses the cache entirely. Obvious in hindsight, confusing for a day.

The admin bundle is heavy. The panel adds real weight to the build. It's code-split away from the storefront so visitors never pay for it, but cold builds got slower and the client's CI minutes noticed.

Versioning discipline matters. Because collections are code, two developers editing the same collection on different branches can produce migration conflicts that a SaaS CMS structurally can't have. We added a rule: collection changes ship in their own small PRs.

When I'd Still Choose Something Else

If the client has no developers and never will, a hosted CMS with a support contract is the kinder choice. If content is one humble "about" page, hardcode it, a CMS of any kind is ceremony. And if the team is already deep in another CMS with hundreds of documents, the migration cost usually beats the architectural niceness.

But for the project I keep getting hired for, a Next.js storefront on Medusa with a marketing team that wants real editing power, Payload has displaced everything else I used to reach for. The stack collapses to TypeScript, Postgres, and one deployment. Fewer moving parts, fewer bills, and the editor experience is good enough that clients stop asking for changes and start making them.

Related posts