Multi-tenant (SaaS) API with multiple databases #63848
Replies: 2 comments · 2 replies
-
|
hey, the approach Codex gave you is actually solid and pretty much the recommended way to do this in modern Symfony/Doctrine. the DBAL middleware layer is exactly where runtime connection switching belongs since Doctrine DBAL 3.x introduced that extension point. you're on the right track. a few things to watch for in production though: metadata cache - since all your tenants share the same schema this should be fine. but if you ever end up in a situation where you're rolling out migrations tenant by tenant (not all at once), cached metadata from one tenant could be stale for another. if that ever becomes a concern, you can namespace the Doctrine metadata cache key by tenant id. for now with identical schemas its a non-issue. connection lifecycle - PHP-FPM is actually your friend here. each request gets its own process, connections are request-scoped and torn down at the end. theres no risk of leaking one tenant's connection state to another request. just make sure you're NOT using messenger workers - the entity manager state - in long-running messenger consumers, call one alternative you might consider down the line: instead of reading tenant DB urls from a config file, store them in a central "control plane" database. that way adding a new tenant is just an insert, no config file changes or redeployments needed. your middleware would hit the control plane DB to resolve tenant -> connection string (with aggressive caching obviously). overall though, what you have is clean and production-ready. nice work getting it set up 👍 |
Beta Was this translation helpful? Give feedback.
-
|
Why not one entity manager for each tenant ? And make a wrapper of the doctrine ManagerRegistry to fetch the desired entity manager based on the tenant name or id |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Dear community. First of all I want to express my appriciation for the developers of Symfony. I'm using it for many years and helps me to develop new features pretty quick.
Current Scenario
Currently I have a multi-tenant (SaaS) REST API. Each tenant has its own database, but they all have the exact same schema. For each tenant I have an .env file (e.g. .env.tenant1.local, .env.tenant.env). Each tenant has it own domainname and, so also a VHost within Apache. Within that VHost I override the APP_ENV so that each domain read the correct env file. I have a shell script that iterates over all the tenants to execute the Doctrine migrations (e.g.
APP_ENV=tenant1 php bin/console doctrine:migrations:migrate)The Problem
I have to request SSL certificates for each domain. Also using the Symfony Messenger component, with for example RabbitMQ, means I have to run a process for each tenant. With five tenants that is okay, but the amount of tenants is growing each year. So I'm thinking of moving to one domain for the API and determine the tenant based on a header, e.g.
X-Tenant-ID. However that means I need to switch the database connection on runtime and that is where I got stuck. I know I can configure more connections in the configuration of Doctrine, but that means a redeploy for each new tenant, which I don't want.What I've Tried
I have tried multiple solutions myself (compiler passes and stuff) which worked half. Recently I used ChatGPT Codex and it gave a working solution, but I want to confirm if this is the Symfony way. What it basically did was:
Applicationclass for the console, and added a required input, the tenant ID. Withinbin/consoleit uses this new Application.Application.php
Created two subscribers that catches
kernel.requestandconsole.command, reads the header of input and stores it within aTenantContextservice.It created a
TenantDriverMiddlewarethat implements theDoctrine\DBAL\Driver\Middlewareinterface from Doctrine. Within thewrapmethod it reads the tenant from the context, gets the database url from a config file and calls theparent::connectwith the updated params.TenantDriverMiddleware.php
TenantStamp.It works perfectly. My endpoints work with the
X-Tenant-IDheader, and I can run migations and other commands by runningphp bin/console doctrine:migrations:migrate --tenant=tenant1. It also works with the VichUploaderBundle which I have installed.The Question
Is the solution Codex created good practice? Will I run into caching or connection issues on production? Or are there other ways of solving this problem? I know I could just put everything in one database and add a
tenant_idcolumn to my tables, but that is not an option. I'm really hoping that there is a good way in Symfony to fix this problem.Best regards,
Jan Doornbos from The Netherlands
Beta Was this translation helpful? Give feedback.
All reactions