Skip to main content

Site Operating Model (Primary / Independent / Mirror)

MSK Core Concept β€” Site Operating Model

This page explains the "Site Operating Model" (Concept 1) among Multi-SaaS Kit's 6 core concepts.

In Multi-SaaS Kit, a "Site" is a layer separate from the permission hierarchy (SaaS/Tenant). A site's type is determined by the settings of a SaaS Product or Tenant record. The single source of truth (SSOT) for the resolution logic is SiteTypeHelper (platform/web/laravel/core/Base/Site/Support/SiteTypeHelper.php), and domain routing binding is handled by the ResolveByDomain middleware.

Two types + one role at a glance​

Site types are two (Independent / Mirror); "Primary" is not a separate type but a role attached to the first-created Independent site (ADR-090 β€” the old "three parallel types" framing was rejected).

CategoryDetermined byDomainRoutes/controllers/views"Site Management" shown
Independent (type)settings.site={slug} or settings.domains=[...] (owns folder)Own domainFully separated (app/Sites/{Studly}/)βœ…
Mirror (type)settings.mirror_domains (borrows folder)Own (sub)domain requiredBorrows Independent routing + mode (β… ) branch / (β…‘) mountβœ… (resolves to Independent)
Primary (role)The first-created Independent site (= occupies .env APP_URL host, auto-detected)Main domainShared (/)βœ…

Key point: A site type defines "which domain/code structure serves the request", which is orthogonal to "who manages it" β€” the Permission Hierarchy.

Primary Site​

The single SaaS that occupies the host of .env APP_URL. It is recognized as a manageable site even without a dedicated "independent site slug".

Since v1.30.3 it is auto-detected without operator input β€” SiteTypeHelper::detectPrimarySaas() uses a 4-step priority:

PriorityConditionNote
1settings.is_primary_site=true1.30.0 backward compat (explicit toggle)
2settings.domains matches the .env hostexplicit domain registration
3SaaS slug == host SLDe.g. academy.how β†’ academy
4Single-SaaS project β†’ that SaaSdefault path for a fresh _template
Default behavior for new projects

A project created with make create usually takes path β‘£, so the first SaaS automatically becomes the Primary Site. Therefore every project owns at least one "manageable site" at the SaaS level.

Independent Site​

A fully separate site with its own domain and separated routes, controllers, views, and resources.

Resolution (SiteTypeHelper::isIndependent()):

  • settings.site is a non-default slug (β†’ has an app/Sites/{StudlyName}/ folder), or
  • settings.domains is a non-empty array (explicitly registered own domains)

It is possible at both the SaaS and Tenant levels. The ResolveByDomain middleware reads settings.site at both layers:

LayerMethodBehavior
SaaSbindSaasProduct()saas->settings.site β†’ current.site
TenantbindTenant()tenant->settings.site, otherwise inherits the parent SaaS's site β†’ current.site

So if a Tenant has its own settings.site, it becomes a tenant-specific independent site; otherwise it follows the parent SaaS's site.

For the code structure standard, see the SaaS Module System (app/Sites/{StudlyName}/).

Mirror Site​

A surface variant of an Independent site (belongs to it) β€” it borrows the Independent site's code/routing but must own its own (sub)domain, opt-in. It does not own a folder (borrows), which distinguishes it from an Independent site.

  • Declaration: settings.mirror_domains on the Independent record (a map, e.g. { "bible.scripture.how": "bible" }). This is a key separate from the Independent's settings.domains.
  • Two branching modes (can be combined):
    • (β… ) Per-domain branching β€” same routes, branch theme/logo/some regions by domain. Implementation: settings.custom_theme β†’ ViewThemeResolver searches resources/views/{saas,tenants}/{slug}/ first.
    • (β…‘) Route mount β€” mount a specific route of the borrowed site at the root of the own domain (e.g. bible.scripture.how/ β†’ scripture.how/bible). Implementation: global pre-routing MountMirrorRoute + MirrorHostResolver. Additive with the canonical path (scripture.how/bible) β€” both work.
  • Belonging: the mirror domain resolves to its Independent site, so data, management, and audit auto-belong to the Independent site (not a separate record).
Old definition caution

"Mirror = a domain-less theme overlay" is the old definition, superseded by ADR-090. settings.custom_theme is not the mirror's defining field but its mode-β…  branding mechanism. A mirror always has its own (sub)domain.

The "Site Management" exposure gate​

Whether the "Site Management" menu (built-in boards: notices/FAQ/inquiries, ads/analytics, and other "site-level settings") appears in the operator admin panel is decided by:

SiteTypeHelper::isManageableSite()  (= currentContextIsManageableSite())
β†’ Independent site : true
β†’ Primary site (role of Indep.) : true
β†’ Mirror domain : true ← resolves to the Independent record β†’ naturally included (mirror settings/audit = the Independent's)
β†’ Non-independent Tenant/Org : false ← inherits parent (Independent/Platform) settings

This gate is used by Filament Resources' shouldRegisterNavigation() / canAccess() in the SaaS/Tenant panels.

Which type should I use? (decision tree)​

Need completely different routes/pages/data?
β”œβ”€ Yes β†’ Independent site (settings.site + app/Sites/{Studly}/, own domain)
└─ No β†’ Use the Primary site (the first Independent site) as-is (.env APP_URL)

[Option] Want to also expose an Independent site under its own (sub)domain? (shared routing; only domain/branding/mount differ)
└─ Yes β†’ Add a Mirror site (settings.mirror_domains, opt-in)
β”œβ”€ Branch only theme/logo per domain β†’ mode β…  (custom_theme)
└─ Mount a specific route at the own domain root β†’ mode β…‘ (route mount)

A mirror always sits on top of an Independent site (borrows independent routing; only domain/branding/mount differ). Data, management, and audit belong to the Independent site.

  • platform/web/laravel/core/Base/Site/Support/SiteTypeHelper.php β€” site type resolution, detectPrimarySaas, isManageableSite
  • platform/web/laravel/core/Base/Site/Support/MirrorHostResolver.php Β· core/Base/Routing/Middleware/MountMirrorRoute.php β€” mirror domain resolution + route mount
  • platform/web/laravel/core/Base/Routing/Middleware/ResolveByDomain.php β€” domain β†’ current.site binding
  • Design decisions: ADR-090 (Site model β€” Independent/Mirror two types + Primary role + mirror route mount; supersedes ADR-023's three-type formalization), ADR-024 (SaaS module standard structure), ADR-029 (Project-per-SaaS)
  • Related architecture: SaaS Module System, Multi-tenancy, Permission Hierarchy