SaaS Demo (runnable)¶
A runnable Symfony 7.4 app under examples/saas/ that boots three tenants — acme,
globex, initech — on a single docker compose up. FrankenPHP + Caddy +
MariaDB 11 + Mailpit, all wired together so http://acme.tenancy.localhost/ shows the
Acme tenant page and http://globex.tenancy.localhost/ shows a different one.
The demo is the bundle's living integration test: bin/smoke.sh runs against it on every
push and PR via .github/workflows/demo-smoke.yml, so a green CI badge means the demo
actually boots and resolves tenants correctly on a clean machine.
What this demo proves¶
- Subdomain resolver works out of the box —
*.tenancy.localhostis RFC 6761 magic; modern browsers route it to 127.0.0.1 with no DNS or/etc/hostssetup. - The Profiler tab shows the resolved tenant per request — open the WDT on any tenant page and the Tenancy panel renders slug, driver, bootstrappers, and resolver FQCN.
- Per-tenant SMTP via the Mailer bootstrapper — three
POST /_demo/send-test-mailcalls land in Mailpit with distinctFrom:andReply-To:per tenant. - OriginHeaderResolver fires when the Host doesn't carry a slug — a cross-origin
request to the landlord with
Origin: https://acme.tenancy.localhoststill resolves to Acme via the priority-25 resolver. - The three-step fallback ladder — when
*.tenancy.localhostdoesn't resolve locally (Safari, some WSL2 setups),curl -H "Host: ..."and/etc/hostscover everything.
Quick start¶
git clone https://github.com/danplaton4/tenancy-bundle && \
cd tenancy-bundle/examples/saas && \
docker compose up -d --wait --build
On a warm Docker image cache the stack boots in ~50s; cold (first image pull) ~110s.
--wait returns only when every healthcheck has passed. If *.tenancy.localhost
doesn't resolve in your browser, the demo README walks
through the three-step fallback ladder (curl Host: header → /etc/hosts → browser-native).
A peek at the demo¶
+--------------------------------+ +--------------------------------+
| http://acme.tenancy.localhost/ | | http://globex.tenancy.localhost|
|--------------------------------| |--------------------------------|
| Acme Corporation | | Globex Industries |
| brand: #4a5cff | | brand: #ff6a00 |
| posts: 3 (acme-only data) | | posts: 2 (globex-only data) |
+--------------------------------+ +--------------------------------+
WDT: slug=acme driver=database_per_tenant resolved_by=HostResolver
Same controller code serves both pages; the bundle's HostResolver picks the leftmost
DNS label, DatabaseSwitchBootstrapper swaps the DBAL socket, and Doctrine returns the
tenant's isolated row set.
Full walkthrough¶
The canonical demo doc covers the two-minute boot, the three-step fallback ladder, the Profiler walkthrough, the Mailpit walkthrough, the OriginHeaderResolver scenario, the optional HTTPS-with-Caddy step, and the bundle-source dev loop. See examples/saas/README.md for the full guide.
See also¶
- Installation — install the bundle in your own app
- Profiler tab — Tenancy panel internals
- Mailer bootstrapper — per-tenant SMTP DSN, From, Reply-To
- Origin header resolver — Trust Model + allow-list