---
title: 'Learnings from our multi-tenant Laravel application'
source: 'https://youtube.com/watch?v=Lmope5CdM10'
video_id: 'Lmope5CdM10'
date: 2026-06-15
duration_sec: 0
---

# Learnings from our multi-tenant Laravel application

> Source: [Learnings from our multi-tenant Laravel application](https://youtube.com/watch?v=Lmope5CdM10)

## Summary

The video discusses the architecture and lessons learned from building a multi-tenant food ordering web app in Laravel, which scales to over 1,500 tenants, 1 million monthly visitors, and processes €10 million in orders monthly. The speaker shares insights on database design, query performance, and tenant resolution.

### Key Points

- **Database Approach** [0:20] — Chose a shared single database approach for speed and ease of development, despite risks like full downtime if the database server fails and complexity in ensuring data isolation.
- **Tenant Identifier Columns** [1:05] — Every table includes a merchant_id and team_id to link data to specific tenants, enabling direct relationships and avoiding complex distant relationships.
- **Lesson 1: Avoid Distant Relationships** [1:56] — Distant relationships (e.g., has-many-through) increase query complexity and hurt performance. Refactored to add tenant identifiers on every table for direct queries.
- **Lesson 2: Be Careful with OR WHERE Queries** [4:00] — OR WHERE queries can leak data across tenants if not scoped correctly. Use the where method with closures to add parentheses and ensure proper scoping.
- **Lesson 3: Pay Attention to Indices** [5:32] — Profile queries and add necessary indices (e.g., foreign, composite) before caching. Use EXPLAIN to check index usage. Unique indices must be scoped to tenant.
- **Lesson 4: Don't Use Global Scopes** [6:39] — Global scopes are often removed. Use local scopes instead, applying them manually where needed. Global scopes only for global constraints like archiving old orders.
- **Tenant Resolving** [7:31] — Tenants are resolved via subdomain or custom domain using middleware. Custom domains use a reverse proxy (Caddy) for SSL. API keys are used for API access.
- **Queue Jobs Tenant Context** [9:08] — Jobs dispatched from a tenant context are not tenant-aware by default. Pass the current merchant as an argument to provide context inside the job handler.

### Conclusion

Building a multi-tenant Laravel app requires careful database design, avoiding distant relationships and global scopes, proper indexing, and ensuring tenant context in queues. The speaker may create a follow-up video on handling flash sales.

## Transcript

in this video we'll be taking a look at
how I built a multi-tenant food ordering
web app in laravel I scal this web app
to over 1,500 tenants we get over 1
million of unique monthly visitors and
this web app processes around € 10
million e worth of food orders every
single month so let's dive
in multi-tenant applications command
many shapes and forms and one of the
first decision you'll have to make is
whether you want to have a multi-
database approach or a single shared
database approach for our multi-tenant
food ordering app we went with a shared
database approach for Speed and ease of
development however it's far from
perfect because if our database server
is down it'll impact every single
mergent on top of that a single database
approach means you're constantly doing
mental gymnastics in your codes when
you're doing queries to make sure you're
not accessing data of other tenants I
didn't use any third-party multi- Talent
packages as there simply weren't any
available at the time I started the
project in
2014 in our single database setup every
table has to be linked to a specific
tenant or Merchant in our case so let's
for example take a look at our orders
table we have an ID which it identifies
the order we have a type we have a
consumption time and we have some
customer information like the customer
first name and the customer email to be
able to link this specific order to the
right Merchant we introduced a merchant
ID column in our Merchant model we can
then create a relationship and access
the order on a merchant as follows some
of our Merchants have multiple
businesses for example a franchise so we
also introduced a team ID so we can
easily link multiple Merchants to a
team here are a couple of lessons
learned the hard way when working with
this tenant identifier column lesson
number one is don't use distant
relationships Merchants love love to
look at reports for example which
product was sold the most for the past 7
Days compared to the 7 days before and
our schema looked a little something
like this we had a team table we had a
Merchants table we had an order table
and we had an order products table
Merchants were linked to the team
through the team ID orders were linked
to the merchant and to the team through
the merchant ID and through the team ID
and our order products were linked to
the order through the order ID in an
effort not to repeat myself I didn't
include the merchant identifier nor the
team identifier on the order products
table because I figured I could access
the order products on the merchant
through the orders using a h many
through relationship the way a has many
true relationship works is that we have
our Merchant model that has many orders
and orders has many order products and
we can access the order products through
the orders on the merchant using a has
many true relationship if you take a
look at the merchant model we can see
that we have a direct relation on the
orders using a has many relationship and
order products is a so-called distant
relationship which allows us to access
the order product through the order and
while distant relationships work
beautifully it has a massive impact on
query performance because the complexity
of the query drastically increases
instead we refactored all our distant
relationships and added a tenant
identifier on literally every single
table so in this case by adding a
merchant ID and a team ID we are able to
access order products on the merchant
and even on the team using a has many
relationship this reducing query
complexity this Merchant and team
identifier is applied to every single
table we have in our database schema
because we learned the hard way that
when doing reporting queries it's best
to jump through as few Hoops as possible
lesson number two is to be extremely
careful with orware queries in this type
of single database multi-tenant setup
the orware query should be used with
caution because if you don't scope your
queries correctly you'll leak orders
from other merant let's take a look at
an example in the example shown on
screen we're counting the orders of the
mergent with ID one where the type
equals takeout or where the status
equals completed if you take a look at
at a raw query we'll see that we'll
select the orders where the merchant ID
equals in our case one and the merchant
ID is not null and the order type equals
in our case takeout or the status equals
completed now because of the way MySQL
interprets this query it will find the
orders where the merchant ID equals 1
and the order type equals takeout or it
will take every single order where the
status equals completed also the one of
the other Merchants so it's super
important to scope the query correctly
using the wear method on the orders
relationship using this wear method
we're essentially adding parentheses in
the Raw MySQL Query and if we take a
look at the resulting raw MySQL Query
we'll see that the order type in our
case should be take out or the status
should be completed but we removed the
or statement and we added these
parentheses and this way MySQL is a able
to scope the query correctly lesson
number three is to pay extra attention
to the indices our orders table quickly
grew to tens of millions of rows and
obviously our Merchants expect Snappy
performance on those Peak moments when
they're preparing dozens of orders per
hour to be able to guarantee this it's
important to profile every single query
that gets executed and add the necessary
indices before you even start thinking
about caching a single thing if thing we
always did was add a foreign index to
the merchant ID and the teams ID column
but often we added composite indices to
really squeeze all the performance we
can out of a query to figure out which
index MySQL uses for a query you can add
the word explain before it and in the
result MySQL will tell you which index
it used to execute your query and how
many rows it had to look through unique
indices for example a slug also have to
be scoped to the tenant otherwise you'd
get unique constrainted errors when the
same value already occurs with the other
tenants and lesson number four is don't
use Global Scopes Global Scopes
initially sound like an amazing idea you
can apply a scope to all queries without
repeating yourself however I found
myself removing the global Scopes more
often than not using the query without
Global scope method for example for our
internal reporting we decided to never
use Global scopes for Tenon specific
queries and we only used Global scopes
for Global query constraints like for
example only querying the orders of The
Last 5 Years orders that are older than
that are considered archived and we Mark
those orders with an archived at date
kind of like soft elets and apply a
global scope on it for all other
purposes we opted for local Scopes and
applied them manually where needed
now that we talked about the database
setup let's dive into another big aspect
of a multi-tenant application and that
is tenant resolving we provide a webshop
for every tenant that can be visited
using a dedicated subdomain or a custom
domain we provide every tenant with a
free subdomain out of the box and using
middleware we are able to figure out the
tenant when someone visits the subdomain
so let's take a look at our Merchants
buy domain middleware we have a domain
stable in our database that Maps a
domain or a subdomain to a merchant our
Merchant by doain middleware is able to
figure out which domain or subdomain is
linked to which mergent and then injects
the resolved mergent into the service
container if the mergent is suspended
for example when they don't pay their
invoice we throw an exception and render
a specific view for it and when no
Merchant is found we throw a merchant
not found exception which essentially
renders a 404 when Merchants prefer to
have a custom domain we have a reverse
proxy setup that will handle SSL
certificates and proxying to our system
with minimal effort we use a tool called
CX which makes managing these custom
domains a breeze but more on that in a
future video we also have an API that's
accessible through a tenant specific API
key and in a similar fashion we apply a
merchant by API Key Middle bear that's
able to resolve a merchant from an API
key and instead of the domain we use
this API key in many of our services for
example our iPad Point of Sales app it's
important to pay attention to jobs
dispatched onto a queue from within a
tenant context because they are not
tenant aware by default to provide the
job with the necessary context we always
pass in the current Merchant as an
argument so we're able to access the
merchant model inside of the job Handler
and that concludes the video if you
found this content helpful or insightful
give it a thumbs up and consider
subscribing I may do a second part
because there's just so much more to
talk about I created an entire video
about how a flesh sale on a single
tenant took down our entire system so if
you're interested to learn more about
that specific issue I'll link it in the
description thank you for watching and I
will see you in the next one
