Technical Article

Multi-Tenant CMS: Two Websites on One Platform

Two very different websites share one Laravel CMS and one database. What has to be per tenant, the cache and SEO traps, and how each site is provisioned.

Share this article

A multi-tenant CMS serves several websites from one installation, and each site behaves as if it had the CMS to itself: its own domains, languages, content types, design, menus, forms and search settings. This website is one of those sites. mmuzammilq.com and GrowBizNest, a services and CRM business with a completely different design and content model, run on the same Laravel CMS, the same database and the same deploy. This article covers what has to be per tenant in a CMS, what can be shared, and the CMS-specific traps that a generic multi-tenant app never meets, such as page caches and search engines. The platform decisions underneath it are in SaaS platform architecture: the decisions that come first.

What multi-tenant means in a CMS

A CMS can serve several sites in three ways. A separate installation per site gives full isolation, and a separate upgrade, server and bug fix per site. A multisite feature inside one installation, as some CMSs offer, shares the code but often keeps a lot of state global. A multi-tenant CMS treats each site as a tenant: one codebase and one database, with every piece of content and configuration owned by exactly one tenant and invisible to the others.

This platform is the third kind. One release goes out to both sites at once, and a site can still look and behave nothing like the other.

What is per tenant, and what is shared

The useful exercise is to list every table and decide which side it is on. In this CMS, 45 models are tenant-owned, and the list is longer than most people expect:

Per tenantExamples on this platform
Identity of the site Domains, the primary domain, brand name and settings, status (live or suspended)
Languages Which locales a site has and which is the default. mmuzammilq.com is published in several languages; GrowBizNest only in English
Content model Post types and taxonomies. GrowBizNest has its own types for services, industries and businesses; this site uses the built-in ones
Content Entries and their translations, revisions, media, FAQs, citations, relations between entries
Design Templates, template revisions, theme parts such as header and footer, menus
Interface text Translated interface strings, so one site’s wording never appears on the other
SEO Redirects, 404 logs, schema templates, internal link counts, the sitemap
Everything else Forms and their submissions, leads, social channels and posts, analytics connections, AI credentials, the audit log
Tenant-owned on this platform. The shared side is short: the code, the tenant and domain tables, and user accounts, which get a separate role in each site they can access.

The items in the last three rows are the ones that tend to be left global by accident. A redirect table without a tenant column means a redirect added for one site can capture the other site’s URL. An interface string table without one means a wording change on one site rewrites the other. Each of those is a real leak, just not one that shows up in a security review of the database queries.

Enforcement: the same rule for content as for data

Every tenant-owned model uses one trait that filters reads by the current tenant, stamps new rows with it, and throws when no tenant is bound. That rule matters more in a CMS than in most apps, because a CMS has so many listing queries: menus, related articles, sitemaps, archive pages and search. Any one of them written without the tenant filter would show another site’s content on a public page. With the filter applied by the model, they can’t. Why it should throw rather than return everything is argued in multi-tenant isolation.

Requests find their site by host name. An unknown host is a 404, a suspended site is a 503, and a site’s secondary domains redirect permanently to its primary one, so search engines index each site under one host.

The page cache has to know the tenant

A CMS lives on its page cache, and a cache is the easiest place to mix tenants up. This platform stores rendered public pages as files, laid out by tenant first:

The page cache layout. Bumping a tenant’s generation number invalidates every stored page of that site at once.
{cache}/{tenant}/{generation}/{ab}/{key}.json

Three rules come out of running it. The cache key includes the host, so the same path on two sites can never share an entry. Invalidating a site bumps that tenant’s generation number, which orphans every page of that site in one step and leaves the other site’s cache untouched, so publishing on one site never makes the other site slower. And the cache sits after tenant resolution but before the session starts, so a cached page is served without a database query and without setting a cookie.

The same thinking applies to everything derived from content: sitemaps, robots.txt and llms.txt are generated per tenant, and a cached host lookup stores only the domain’s id, never a whole site object that could go stale.

Different designs on one codebase

mmuzammilq.com and GrowBizNest share no visual design. Each tenant has its own templates, theme parts and menus, stored as content rather than as code, so a new design for one site is a content change on that tenant, not a fork of the theme. Templates are assigned to content types per tenant, and pages are built from those templates. A section of a page is data: the template decides how it looks.

The rule I apply is the same one I use on larger platforms: differentiate tenants with data and modules, never with forks. A site that needs something genuinely new gets a new module that any tenant could turn on, not a branch of the code.

Provisioning: every site is a seeder

Creating a site by clicking through an admin is how two environments drift apart. On this platform, every tenant is provisioned by an idempotent seeder: the tenant record, its domains, settings, languages, post types, media, content, menus, templates and theme assignments. GrowBizNest’s seeder reads its pages from content files in the repository and its images from a folder next to them, and running it twice changes nothing the second time.

  • A new site is reviewed like code, because it is code.
  • Local, staging and production get the same site from the same command.
  • Article seeders record what they last wrote, so an article an editor has since changed in the admin keeps its words on a re-run.

This article itself was added the same way: a content file and a seeder that creates it as a draft, which the editorial calendar then publishes at the site’s own scheduled time.

A checklist for a multi-tenant CMS

CheckWhat a failure looks like
Every content and config table has a tenant column A redirect or menu from one site appears on another
Reads without a tenant throw A sitemap or listing shows every site’s pages
Unknown hosts return 404 A parked domain serves a real site’s content
The page cache key includes the host, and invalidation is per tenant One site’s page is served on another, or one site’s publish clears every cache
Interface text and translations are per tenant A wording change on one site rewrites the other
Each site has one canonical host Search engines index duplicates on secondary domains
Sites are provisioned from seeders Production and local quietly disagree about a site
Each row is a test or a rule on this platform.

Where to go next

If you are choosing between one installation per client and one shared platform, single tenant vs multi tenancy compares the two from both sides. For a review of a CMS or platform that serves several brands or clients, see SaaS architecture.

Share this article

References

  1. Laravel documentation: Eloquent global scopes
  2. Google Search Central: consolidate duplicate URLs

Evidence and further reading

All insights →
InsightSaaS Platform Architecture: The Decisions That Come FirstSeven decisions a multi-tenant platform has to make before features, in the order they bind each other, with what each one cost on four platforms I run.

Facing a similar problem?

Platform & Integration Review

For SaaS founders and engineering leads whose platform is getting slower and riskier to change.

Request an assessment

Let's build what's next

Have a complex system that needs to be built right?

Whether you are starting from an idea, replacing an existing platform, or scaling a system, let's talk.

Better Technology.
Brighter Possibilities.