Building a Multi-Tenant Application with Symfony: A Guide
When building SaaS (Software as a Service) platforms, it’s essential to manage multiple tenants effectively. This means supporting multiple stores or clients using the same application while ensuring that each tenant has a distinct environment for their data, configurations, and, in some cases, custom features.
In this blog, we’ll explore how to build a multi-tenant application using Symfony, focusing on a shared database approach that’s simple, flexible, and scalable.
What is Multi-Tenancy?
In multi-tenancy:
- A tenant is a distinct user or client of your SaaS app.
- Each tenant has access to their own data, settings, and sometimes their own customizations.
- Shared Database: Multiple tenants share the same database, but data is logically separated using a
tenant_id
to identify records that belong to each tenant.
Why Symfony for Multi-Tenancy?
Symfony is one of the most flexible and robust PHP frameworks. It allows for customizations at the routing, database, and service layers — making it an excellent choice for building multi-tenant SaaS applications.
Approaches to Multi-Tenancy in Symfony
There are two common approaches:
- Separate Database per Tenant — Each tenant has its own database.
- Shared Database with Tenant Isolation — All tenants share the same database, but their data is isolated via a
tenant_id
.
In this guide, we will use the Shared Database with Tenant Isolation approach, which is more efficient and scalable when you have a large number of tenants.
Step-by-Step Guide to Building a Multi-Tenant SaaS with Symfony
1. Define Your Tenant Model
First, we need to define a Tenant
entity that stores the essential information for each tenant.
// src/Entity/Tenant.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Tenant
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @ORM\Column(type="string", length=255)
*/
private $customDomain; // e.g. store1.yoursaas.com
// Add other fields as needed (e.g. logo, theme, etc.)
}
This model represents a tenant in your application, where the customDomain
can be used to map the tenant’s domain to their specific store.
2. Detect the Tenant from the Domain
You’ll need to detect the tenant based on the domain or subdomain of the incoming request. This is typically done using Symfony’s request listener.
Here’s an example of how to extract the domain and match it to a tenant:
// src/EventListener/TenantResolverListener.php
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Repository\TenantRepository;
class TenantResolverListener
{
private $tenantRepository;
public function __construct(TenantRepository $tenantRepository)
{
$this->tenantRepository = $tenantRepository;
}
public function onKernelRequest(RequestEvent $event)
{
$host = $event->getRequest()->getHost();
$tenant = $this->tenantRepository->findOneBy(['customDomain' => $host]);
if (!$tenant) {
throw new NotFoundHttpException("Tenant not found for domain: $host");
}
// Store the tenant in the session or context
// This can be used later to filter tenant-specific data
$event->getRequest()->attributes->set('tenant', $tenant);
}
}
- Here, we’re using the
Host
header from the HTTP request to get the domain (store1.yoursaas.com
, for example). - We then look up the tenant by matching the
customDomain
field in theTenant
entity. - If no tenant is found for the domain, an exception is thrown.
3. Use a Doctrine Filter to Isolate Tenant Data
Once you’ve detected the tenant, you need to ensure that all database queries are filtered by the tenant’s tenant_id
. The easiest way to do this is to use a Doctrine filter.
// src/Doctrine/Filter/TenantFilter.php
namespace App\Doctrine\Filter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class TenantFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $alias)
{
// Only filter entities that have a tenantId field
if ($targetEntity->hasField('tenantId')) {
return sprintf('%s.tenant_id = %d', $alias, $this->getParameter('tenant_id'));
}
return '';
}
}
This filter automatically adds the tenant_id
to every query, ensuring that tenants can only access their own data.
In your services configuration, you would register the filter like this:
# config/services.yaml
services:
App\Doctrine\Filter\TenantFilter:
arguments:
$parameter: '@tenant_id'
4. Update the Routing Based on Tenant Domain
In a multi-tenant setup, you can use subdomains to route requests to different tenants. You’ll need to configure your NGINX or web server to route the requests based on subdomains (e.g., store1.yoursaas.com
).
Example NGINX configuration:
server {
listen 80;
server_name ~^(.*)$;
location / {
proxy_pass http://localhost:8000; # Symfony app
proxy_set_header Host $host;
}
}
This configuration accepts all domains and forwards them to your Symfony app. Symfony will then resolve which tenant to serve based on the host.
5. Handling Custom Domains
If your tenants want to use their own custom domains (e.g., mystore.com
instead of store1.yoursaas.com
), you can:
- Allow users to add their custom domain via the admin interface.
- Store the custom domain in the
Tenant
entity. - Add DNS and SSL configurations for custom domains (via Let’s Encrypt or another service).
- Map the custom domain to the correct tenant using the previously discussed domain detection logic.
Conclusion
Building a multi-tenant SaaS app with Symfony allows for great flexibility in scaling, isolating tenant data, and customizing tenants’ experiences. By using a shared database with logical isolation (via tenant_id
), you can manage multiple clients while keeping things efficient and scalable.
With the steps above, you can:
- Detect tenants by domain
- Automatically filter tenant data in your database queries
- Handle custom domains for clients
This architecture is ready to scale as your platform grows, allowing you to add features like:
- Custom themes
- Tenant-specific configurations
- Dedicated services or databases per tenant (if needed later)
By starting with a strong foundation like Symfony and keeping things flexible from the start, you’ll be in a great position to grow your SaaS app.
Leave a Reply