Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

upnorthal's avatar

Sessions in Multi Tennant App

Imagine a multi tenant market place app exists for Laravel.

Two tenants (Howard and Smith) are signed up to this app, and both tenants have their own unique URL's - that point to their respective store fronts for selling goods.

/s/smith/storefront
and
/s/howard/storefont

Howard and Smith are in no way related. They run entirely seperate businesses and separate store fronts - the only thing they have in common is using the Laravel based app to host their stores.

How should sessions be handled in such a setup? Whats happens if a customer adds a load of items to the cart in the Smith storefront, and then changes URL to the howard storefront? Clearly we don't want the same cart contents to remain.

I would have thought that the initial request to say /s/smith/storefront results in a tenantId getting written to session. Each subsequent request a comparison is made to check the tenantId matches the URI submitted - if it doesn't, take action.

Would something like this be best handled in a custom Middleware ? Anyone have any examples of where this has been done?

Cheers Al

0 likes
10 replies
SaeedPrez's avatar

@upnorthal like you mentioned, you could simply use the tenantId to keep the session data apart, which would allow the user to navigate back and forth between any number of storefronts and keep their separate cart.

// Let's assume a user visits Howard's storefront which
// has $tenantId = 1 and puts a few items in the cart
session()->put($tenantId . '_cart', ['array', 'of', 'items']);

// Then the user visits Smith's storefront, which
// has $tenantId = 2 and puts a few items in the cart
session()->put($tenantId . '_cart', ['items', 'in', 'array']);

Each session key will be unique to the storefront. You could do basically the same thing with cookies too, offer to remember the user's cart until next time.

upnorthal's avatar

Thanks. Where abouts would you inspect the URI in order to check whether the session holds the correct Tennant id? I'm guessing the controller is not the best place?

luceos's avatar

Why don't you modify the session.prefix during runtime, eg in a middleware that also identifies the tenancy environment? If these session values have to remain seperated this might be easiest. Do know that switching tenants while remaining logged in will possibly not work.

A good solution between both of these is to use a custom Session class that always prefixes the tenant Id.

martinbean's avatar

@upnorthal Why not just scope your session cookie to the relevant domain?

config('session.domain', 'tenant1.com');
upnorthal's avatar

Not a bad idea @martinbean. I wasn't planning on giving tennant's their own subdomain though.

Thanks all. It seems that middle ware is the correct place to inspect the tenantId value in the session.

I would not anticipate a use case where a user (member of the public) would be flicking from one store front to another in their browser. As such, if a change of store front does occur, then the session should be totally reset.

I have some reading to do on how Laravel implements middleware! :o)

I like Matt's write up here https://mattstauffer.co/blog/laravel-5.0-middleware-filter-style.

Having read a little on how the middleware requests are 'chained' I suspect my middleware would need to sit at the following point in the stack and be a "pre-middleware " middleware:- (based on my understanding that I need the session support has to have been loaded first)

protected $middleware = [
        'App\Http\Middleware\MyMiddleware',
        'Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode',
        'Illuminate\Cookie\Middleware\EncryptCookies',
        'Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse',
        'Illuminate\Session\Middleware\StartSession',
        'Illuminate\View\Middleware\ShareErrorsFromSession',

                            //  <<<------ My middleware goes here to inspect the tennantId in the session matches the current request URI.

        'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken',
    ];

I would most probably create my own Middleware group for the Storefront routes.

Taylor has slightly confused me here https://laravel.com/docs/master/middleware#terminable-middleware with table about terminable middleware. I can't work out whether my middleware would need to be terminable or not.

All I need to do is inspect the current session and if the tennantId matches the URI, then fine. If it doesn't, then session::flush ; before finally setting the tennantId correctly.

upnorthal's avatar

Okay, moving away from the store front example to something I am actually coding. This is still for a multi tennant app.

An event name should get passed in to the controller. Before it gets to the controller, we need to set the event name against the session.

If the eventName key doesn't exist in the session, then create it and put in the eventName. If the eventName key does exist, but the value doesn't match the $request->eventName ; then remove the existing value and insert the current one (as per this request)

routes.php


Route::group(['middleware' => ['events']], function () {
    //Event routes...
     Route::get('e/{eventName}', 'EventController@show');
       Route::resource('e', 'EventController');
});

Kernel.php

 protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
        ],

        'events' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \App\Http\Middleware\EventGuardMiddleware::class,      //My new middleware
        ],

        'api' => [
            'throttle:60,1',
        ],
    ];

EventGuardMiddleware.php

?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Session;

class EventGuardMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {

        //if session eventName key matches the request eventName, then all is well

        if (! Session::get('eventName')  == $request->eventName)
        {
            Session::forget('eventName');
            Session::put('eventName',  $request->eventName);
            Session::save();
        }




        return $next($request);



    }
}

What happens is that the key eventName and associated value gets written to the session on the first request. If a subsequent request is made - with a different eventName parameter passed via the route - the session key eventName is not getting changed.

I'm wondering if what I am doing is conflicting with the Laravel StartSession middleware - which is 'terminable' - ie the session isn't written until the request is sent to the browser. (on a side note - I don't understand why the session can't be written until the response is sent)

Whats my schoolboy error this time? :o)

upnorthal's avatar

basically, it needed the ! moving.

 if ( Session::get('eventName')  !== $request->eventName)

schoolboy error I'm afraid

vercoutere's avatar

This should just work without modifications. Sessions are domain specific by nature. A session for domain-a.com will not get carried over to domain-b.com, so there's basically nothing you should do.

upnorthal's avatar

@vercoutere

but if I'm using just one domain ie www.mysaasapp.com/s/storefront1 and www.mysaasapp.com/s/storefront2 I do need to handle sessions don't I ?

:o)

vercoutere's avatar

Ah yes, I quite hastily read your post and automatically assumed you were separating your tenants by domain/subdomain.

I do think there should only be one unique session per domain, so I would probably add an entry in this session for each tenant then.

Following the cart example from your post this would be a simple but effective solution:

Route::get('carts/{tenant}', function($tenant) {
    // Get cart from session
    Session::get('carts.{$tenant}');
    // Add cart to session
    Session::put('carts.{$tenant}', $cart);
});

Please or to participate in this conversation.