Let’s say, we have a web app, powered by Laravel and Postgres, that runs across different countries in South East Asia. Each venture will have their own users, products, categories etc. so to make it more manageable we want to have a separate database for each venture. But since we are using Postgres we can leverage schemas which I will describe shortly.
I am using Laravel 5.5 and PostgreSQL 9.5 for my setup. You will find me using the word
Tenant
orVenture
interchangeably to represent the country.
The first thing we want to do in our multi-tenant web-app is to identify the venture/tenant from whom the request has been made.
Create a file called ventures.php
inside config
folder and add a mapping for your ventures. The left-hand side is the domain name and the right-hand side is the schema for respective domain.
Now, create a file called Venture.php
inside app/Library
folder (or you can use any other folder you like; adjust namespace as required). This class will have a list of all the available ventures and helper methods to resolve venture from request URL & to load venture configs.
resolveVenture
method in above piece of code resolves venture from the request URL. The first line of the method gets request URL without the protocol (example.com instead of https://example.com). Then we get all valid venture domains we added earlier to compare from. Finally, we return the database schema if it’s available, otherwise, we switch to the public
by default (or any other value that is defined in your .env
).
loadVentureConfigs
method loads or replaces configs that are venture specific. We will place all venture specific configs inside a folder called venture_configs
at the root level. loadVentureConfigs method will check if the config file is available for given venture and loads it. We are using second parameter Repository $config
because we do not want to override global config repository object. If the second parameter is provided, we will set respective config object instead of the global one. This will help us to get and set venture specific configs later.
Global config repository will always hold config from the current venture as per our design.
Venture specific configurations
Inside these files, we will use dot notation provided by Laravel to override config values. This is how the entries in your venture specific configuration files will look like:
venture_configs/ph.php
Note: You can also separate configs inside the same file using venture name as a prefix. I prefer to keep it separate.
Next thing we need is the config loader. When Laravel is loading configuration for the first time, we somehow need to tell it to load configuration for the current venture as well. We can do so by overriding Illuminate\Foundation\Bootstrap\LoadConfiguration
class which is responsible to load configuration files.
Create a file called ConfigLoader.php
inside app/Bootstrap
directory and add following code.
Here we are extending Illuminate\Foundation\Bootstrap\LoadConfiguration
class to override loadConfigurationFiles
method. Notice Line #19 that loads venture specific configs by calling loadVentureConfigs
method we added earlier.
Confused how $app['venture']
works here? Well, I just added an alias in bootstrap/app.php
to make it more readable.
$app**->alias(\App\Library\Venture::class**, 'venture');
One last remaining thing is to tell Laravel to use our ConfigLoader class instead of Illuminate\Foundation\Bootstrap\LoadConfiguration. We can do so by using ServiceProvider. No, not really! If you dive deep into Laravel’s code you will notice that configs are loaded prior to the service providers.
Illuminate\Foundation\Http\Kernel.php
So, in order to override LoadConfiguration class, we need to use application bootstrapper (bootstrap/app.php
) which is loaded at the very beginning of application bootstrapping process.
Above statements appears just before return $app;
statement in bootstrap/app.php
file.
Since we can now identify tenants and load configurations for the specific tenant, the only thing remaining is database segregation. There are different ways to segregate data for a multi-tenant application. We can either use different databases for each tenant, or use key-value pairs in every table to represent tenant, or, use different schemas. Every method has their own pros and cons which I will not go through in this tutorial.
Since we are using Postgres, we can leverage Postgres Schemas. Schemas are like folders and can hold tables, views, functions, sequences, and other relations. By leveraging schema, we will limit cost and complexity while still maintaining performance and data security. So, instead of creating multiple databases, we can create one database and different schema for all ventures running the app.
Create VentureServiceProvider
and register it in config/app.php
.
In register
method, we will identify the venture and create database instance as necessary. In the first line (Line #18) we are extending the database object to use custom database object we provide. The second line changes pgsql
schema config to the venture we resolved based on URL. Finally, we create and return a new DatabaseManager object which points to the schema of current venture.
db alias points to the
_Illuminate\Database\DatabaseManager_
object. So overriding db will override DatabaseManager as well.
At this point when you use database object with app('db')
or DB::
façade or any other method provided by Laravel, you will always get the connection for current tenant or venture.
So, how can we access venture specific database schemas? Say, we need to perform some operation in th
schema while we are connected to vn
schema? or we need to run some job using the command line?
In VentureServiceProvider
, we can create a database object for all schemas and push it into a service container. We can do the same for configs as well.
You might think that in above implementation we should have changed the configuration of venture also while switching the database after Line #9 instead of setting it in a new key. For instance,
Now when we do app('db.th')
it will switch to config of a respective venture as well. But NO, we must not do this as it will change the state of your application without even realizing which can lead to errors. Moreover, we need to reset config for ventures every time we do some database operation on another venture.
The state of your app should be predictable at any given point of execution.
Continuing on our first implementation, we can get global or venture specific db and/or configurations as described in the following snippet.
Note that at this point we can only use config()
helper or app('config')
provided by Laravel to get configs of current venture.
This keeps our app in more predictable state.
I have a database in place with schema for all 6 ventures. I created a table named search_path
with a column path
inside that table for all ventures. The path column will have a value of venture representing the country. For eg, th
schema will have value Thailand
in path
column.
routes/web.php
Here, we are using Laravel’s default welcome.blade.php
file as a view. The only thing changed is we have added value for the venture that we fetched in our route. And added the timezone config for respective venture.
welcome.blade.php
Let’s open mt.local.th
which is our local domain for Thailand venture. We should get proper venture name from the database and proper timezone from the config.
mt.local.th
Now, let’s try with another domain mt.local.ph
which is our local domain for the Philippines. Notice the venture name and timezone.
mt.local.ph
That’s it! You have successfully created a multi-tenant app using Laravel and PostgreSQL. It may not do a whole lot yet, but it creates a strong foundation for your multi-tenant application. Happy Coding!
In the next part, we will see how we can extend Laravel’s migration implementation to gracefully handle migration for all schemas of our multi-tenant app.