Shane Bogue Projects

Payload CMS platform

A multi-tenant headless CMS built to serve many sites from one codebase. Not deployed yet.

The Exterior Remodelers of America logo on a white panel.
A tenant logo prepared for the branded admin sign-in, where each site's editors will see their own mark and colors. Fig. 1 2026–present
  • Platform
  • In progress
Year
2026–present
Role
Architect and sole developer
Org
Krasiva Windows & Doors
Stack
Payload CMS 3.87, @payloadcms/plugin-multi-tenant, Next.js 16, React 19, TypeScript and 9 more

A single headless Payload CMS backend designed to serve many sites. Each site is its own Next.js or Astro app in one pnpm and Turborepo monorepo and reads its content through a typed SDK. Once it is deployed, each site's editors will sign in to an admin that carries their own logo and colors, and edit only their own site. Two sites, ERA and XRF, are ported to it and run locally. It is not deployed yet.

Problem

The goal is one codebase to replace managed WordPress for a group of sites, where each site's owner makes small edits without asking a developer. The first attempt forked a template per site onto a shared database. The forks drifted: several codebases wrote to one schema, some tables existed that no repo knew about, and fixes made upstream could not reach the forks.

Approach

Three passes, each written down. First, a custom kernel on Payload with a module SDK, a transactional outbox, an event log and a worker. Second, the stock Payload website template forked per client onto a shared Supabase database. Third, after a written audit in October, a rebuild: one headless CMS app on Payload's multi-tenant plugin, the anonymous API closed, server-only API keys per site, upload fields limited to the same tenant, per-tenant admin branding through Payload's own extension points, a publishing queue that revalidates cache tags and calls deploy hooks from Vercel Cron, signed live-preview links, S3-compatible media storage with tenant-prefixed keys, generated migrations with a drift check, and a tenant-isolation test suite that gates every release. Every decision goes into a numbered log.

Process

9 dated steps, from the project’s history

  1. 01

    Started "Stronghold"

    A multi-tenant Payload 3 and Next.js platform. Public sites resolve by hostname and admin traffic by membership.

  2. 02

    Kernel phase 1 done

    Module SDK with boundary checks at boot, a transactional outbox with event log and worker, contacts and memberships, storage and email providers, audited staff sessions, and a Docker image with web, worker and migrate roles. 72 unit and 85 integration tests passing.

  3. 03

    Threw the kernel out

    Replaced it with the clean Payload 3.87 website template and locked the scope: WordPress-style sites on their own domains, sharing one codebase.

  4. 04

    One fork per client

    Shared blocks, branding from environment variables, upstream sync, and tenants and sites on a shared Supabase project. The ERA and XRF forks started here.

  5. 05

    The audit

    I wrote a full audit of the platform. It mapped all the custom code layered over the template and found a stock seed route that could wipe every tenant and two ways to gain admin rights. I fixed those in both forks the same day and retired the fork model.

  6. 06

    v2 foundation

    pnpm and Turborepo monorepo, a headless apps/cms, the multi-tenant content model and access rules, a block library, a generated baseline migration, the tenant-isolation suite and CI. The isolation tests are never relaxed, only extended.

  7. 07

    SDK, branding, publishing, two sites

    A server-only SDK with cache tags, revalidate, preview and media handlers; per-tenant admin branding and a client dashboard; branded admin hosts; the publishing queue on Vercel Cron; live preview. ERA and XRF were ported to headless Next.js apps with content importers the same day. See Fig. 1, above.

  8. 08

    Renamed to Payload CMS

    Dropped the Stronghold name so anyone who picks the code up finds Payload's docs and community. Packages became @cms/* and the migrations were regenerated as one baseline.

    A small dark shield icon with a crenellated top edge.
    The retired "Stronghold" shield, shown in the admin from October 13 until the rename on October 20, 2026. Fig. 2
  9. 09

    Real tenant branding, on a branch

    Real job photos for ERA and XRF are on main. On a branch not yet merged, the importers also bring in the real company logos and set the admin brand colors from them.

Results

The CMS, the SDK and the ERA and XRF site apps run together as one monorepo. CI runs typecheck, lint, SDK tests, generated-file checks, migrations, a drift check, the isolation suite and the build on every change.

Lesson

Forks made with GitHub's template button share no history, so fixes can't be merged back. Several codebases on one schema drift in ways no single repo can rebuild. Much of my first custom layer re-implemented things Payload already does, so v2 uses Payload's own features first. And in Payload 3.87 the multi-tenant plugin adds no tenant filter for anonymous requests, so isolation has to come from access rules and be proven by tests.