> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom session drivers in packages

> Examine Laravel 13's SessionManager to understand the SessionHandlerInterface storage contract, explicit driver selection, TTLs and serialization, retained Stores, and testing.

To provide your own session storage from a package, separate **registering the storage implementation** from **the application choosing to use it**. Installing the package alone should not switch where login state is stored; design it so users can review the impact on existing sessions before enabling it.

This page is based on the official source of Laravel Framework `v13.39.0`. For basic usage, see [Session](https://kawax.biz/en/session.md). For automatic provider loading, see [Package discovery](https://kawax.biz/en/advanced/package-discovery.md).

## extend receives a handler, not a Store

The callback passed to `Session::extend()` returns a **storage handler** that implements PHP's `SessionHandlerInterface`. It is not an API for returning a Store that implements `Illuminate\Contracts\Session\Session`, nor a cache Repository.

`SessionManager::callCustomCreator()` passes the callback's return value to `buildSession()`, which wraps it in a `Store` or an `EncryptedStore` depending on `session.encrypt`. At that point it also applies the cookie name and `session.serialization`.

```mermaid theme={null}
flowchart TD
    A["Register a custom driver name in boot()"] --> B["The user selects session.driver"]
    B --> C["The first driver() call runs the creation callback"]
    C --> D["Return a SessionHandlerInterface"]
    D --> E["SessionManager wraps it in a Store or<br>EncryptedStore"]
    E --> F["StartSession starts loading"]
    F --> G["The application operates on it with put() and so on"]
    G --> H["The Store serializes the data and<br>passes it to handler.write()"]
```

In normal HTTP handling, the `StartSession` middleware manages starting and saving the session. Your package does not need to call `session_start()` or `session_set_save_handler()` to provide a handler.

## Think about the storage contract in separate parts

When you implement your own backend, fulfill the following roles.

| Method | Handler responsibility |
| - | - |
| `open($savePath, $sessionName)` | Any necessary initialization. Return `true` if none is needed |
| `close()` | Any necessary cleanup. Return `true` if none is needed |
| `read($sessionId)` | Return the stored string. Return an empty string if nothing is stored or it has expired |
| `write($sessionId, $data)` | Store the given string and return success or failure |
| `destroy($sessionId)` | Delete the data for the given ID |
| `gc($lifetime)` | Delete expired data based on a lifetime in seconds. No deletion is needed if the storage expires data automatically |

Because the `Store` handles serialization and restoration, the handler must not `serialize()` or `json_encode()` the session array again. When encryption is enabled, store the string processed by `EncryptedStore` as is.

> [!WARNING]
>
> Distinguish missing data from backend failures. In the code reviewed, `Store::readFromHandler()` treats an empty or `false` read result as an empty session. In addition, `Store::save()` does not check the return value of the handler's `write()`.
>
> If your package must always treat failures as exceptions, make the policy explicit and test it: do not swallow exceptions from the storage, and for backends that return `false`, detect that in the handler and convert it to an exception. Laravel does not necessarily retry automatically or report a failed save.

## Register only the creation logic in boot

The following minimal example reuses the built-in `CacheBasedSessionHandler`. It does not implement a new storage protocol; it shows how to register a custom driver that uses a named cache store. `Acme\SessionBridge\` maps to `src/` via Composer PSR-4.

Create the provider in `src/SessionBridgeServiceProvider.php`.

```php theme={null}
<?php

namespace Acme\SessionBridge;

use Illuminate\Contracts\Container\Container;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;
use InvalidArgumentException;
use SessionHandlerInterface;

final class SessionBridgeServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Session::extend('acme-cache', function (Container $app): SessionHandlerInterface {
            $config = $app->make('config');
            $store = $config->get('session.store');
            $minutes = (int) $config->get('session.lifetime', 120);

            if (! is_string($store) || $store === '' || $minutes <= 0) {
                throw new InvalidArgumentException(
                    'acme-cache requires a named cache store and a positive session lifetime.'
                );
            }

            return new CacheBasedSessionHandler(
                $app->make('cache')->store($store),
                $minutes,
            );
        });
    }
}
```

`Session::extend()` retrieves the SessionManager and registers the creation logic, but the cache store inside the callback is not resolved at this point. The storage is created when the driver is first selected. Do not run connection checks or store data in the provider's `boot()`.

If you use your own `SessionHandlerInterface` implementation, swap out the handler the callback returns. If you go as far as `new Store(...)`, the return value no longer matches what SessionManager expects.

* Load the provider as a normal, non-deferred provider.
* Complete registration before the session is first used. If another provider resolves your custom driver earlier, check the loading order as well.
* Use a package-specific driver name. Calling `extend()` with the same name overwrites the creation logic, and registering a built-in driver name takes precedence over the built-in implementation.

## Let the consuming application select it explicitly

Users change the relevant keys in their existing `config/session.php` and leave the other settings in place.

```php theme={null}
'driver' => 'acme-cache',
'store' => 'acme-sessions',
```

For the storage, add a named store to the existing `stores` array in `config/cache.php`. The following example uses the Redis connection `default`. The application is responsible for preparing [the Redis connection settings and the required client](https://kawax.biz/en/redis.md).

```php theme={null}
'acme-sessions' => [
    'driver' => 'redis',
    'connection' => 'default',
    'prefix' => 'acme-sessions:',
],
```

In this example, the connection and prefix are determined by **the cache store configuration**. Because it does not go through `createRedisDriver()` of the built-in Redis session driver, adding `session.connection` or `session.prefix` is not automatically applied by this callback.

This callback does not modify the Repository or the storage configuration; it uses the named store as is. Even a test `array` store shares the same storage within the same application.

Do not unconditionally overwrite `session.driver`, the cookie name, or the application-wide cache prefix from your package. Also confirm with users that the storage is not shared with other uses and that multiple servers can use the same storage.

> [!TIP]
>
> If all you need is ordinary Redis or database storage, start with the built-in session drivers. A custom driver is an extension point for when you need a new storage backend or additional storage conventions.

## Preserve TTL units and the storage format

The `CacheBasedSessionHandler` constructor receives the lifetime in **minutes**. `write()` passes `$minutes * 60` to the cache Repository's `put()` every time, so in the example above the TTL is refreshed on every write. `gc()` returns `0` and leaves expiration to the cache.

On the other hand, the lifetime that `StartSession` passes to the handler's `gc($lifetime)` is in **seconds**. Do not write an implementation that stores the `session.lifetime` value as if it were seconds.

The following changes also involve more than renaming a setting.

| Change | Impact on existing sessions |
| - | - |
| Storage, connection, or key prefix | Old data is no longer visible from the new storage |
| `session.serialization` | Data in the old format can no longer be restored in the new format |
| `session.encrypt` | Reading and writing of plaintext and ciphertext no longer match |
| `session.cookie` | The mapping to the existing session ID cookie sent by the browser changes |

If a package update changes any of these, consider guidance for re-authentication, whether a migration is needed, and deployments where old and new versions run side by side. As with [Package versioning and compatibility](https://kawax.biz/en/advanced/package-versioning.md), treat the storage format and configuration keys as part of the public specification you maintain.

## Changing configuration alone does not replace a created Store

`Manager`, the parent class of SessionManager, retains created drivers by name. In addition, `SessionServiceProvider` registers `session.store` as a singleton.

| Operation | What happens | What does not happen |
| - | - | - |
| Change only configuration values | Future creation can read the new values | The TTL or connection of an already created handler changes |
| Re-register `extend()` under the same name | The creation callback is replaced | The Store retained under the same name is discarded |
| `forgetDrivers()` | All drivers retained inside the Manager are removed | The resolved `session.store` or Store references held elsewhere are replaced |

`forgetDrivers()` is not an API for deleting session data either. Recreating the Store during an active request causes inconsistencies with data already loaded and with other references.

The CacheManager in this example also retains named stores. If you change that connection configuration, re-resolving only the SessionManager is not enough.

In deployments that use configuration caching, rebuild the cache, and restart long-lived workers so they load the new code and configuration. Restarting the application and migrating stored data are separate tasks.

## Session storage and request locking are separate

Implementing a custom handler alone does not serialize concurrent requests for the same session. `StartSession` blocking uses locks from the cache store selected by `session.block_store`, which is not necessarily the same as the storage for the session itself.

If you control this with something like `Route::post(...)->block()`, separately verify a lock-capable cache store, sharing across multiple servers, and the wait and hold durations. Do not let your package enable application-wide blocking settings on its own.

## Test through saving and reloading

In packages that use [Orchestra Testbench](https://kawax.biz/en/advanced/package-testing.md), load the provider and write tests that actually select your custom driver. The following is an example `tests/SessionDriverTest.php`. The test `array` cache does not use any external connection.

```php theme={null}
<?php

namespace Acme\SessionBridge\Tests;

use Acme\SessionBridge\SessionBridgeServiceProvider;
use Illuminate\Session\CacheBasedSessionHandler;
use Illuminate\Session\Store;
use Orchestra\Testbench\TestCase;

final class SessionDriverTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [SessionBridgeServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('session.driver', 'acme-cache');
        $app['config']->set('session.store', 'session-test');
        $app['config']->set('session.lifetime', 2);
        $app['config']->set('session.cookie', 'acme_session');
        $app['config']->set('session.serialization', 'json');
        $app['config']->set('session.encrypt', false);
        $app['config']->set('cache.stores.session-test', ['driver' => 'array']);
    }

    public function test_it_wraps_the_registered_handler(): void
    {
        $store = $this->app->make('session')->driver();

        $this->assertInstanceOf(Store::class, $store);
        $this->assertInstanceOf(CacheBasedSessionHandler::class, $store->getHandler());
        $this->assertSame($store, $this->app->make('session.store'));
    }

    public function test_it_saves_and_reloads_the_payload(): void
    {
        $manager = $this->app->make('session');
        $first = $manager->driver();
        $first->start();
        $first->put('cart.quantity', 3);
        $first->save();

        $id = $first->getId();
        $payload = $first->getHandler()->read($id);
        $this->assertSame(3, json_decode($payload, true)['cart']['quantity']);

        $manager->forgetDrivers();
        $second = $manager->driver();
        $second->setId($id);
        $second->start();

        $this->assertNotSame($first, $second);
        $this->assertSame(3, $second->get('cart.quantity'));

        $second->invalidate();
        $this->assertSame('', $second->getHandler()->read($id));
    }

    public function test_forgetting_drivers_does_not_replace_the_store_binding(): void
    {
        $old = $this->app->make('session.store');
        $manager = $this->app->make('session');

        $this->app['config']->set('session.lifetime', 5);
        $manager->forgetDrivers();

        $this->assertNotSame($old, $manager->driver());
        $this->assertSame($old, $this->app->make('session.store'));
    }
}
```

This test is an **isolated test** that recreates the Store within the same application. It does not verify HTTP middleware or cookie round trips, persistence across processes, or actual Redis expiration and locking. Do not use `array` as shared production storage.

Before publishing your own handler, also add tests for the following.

* Unsaved or expired IDs return an empty string
* A stored string is restored unchanged and cannot be retrieved after deletion
* TTL units and refreshes, and expiration on the real backend
* Read, write, and delete failures produce the defined return values or exceptions
* Round trips with `session.encrypt` enabled, and the supported serialization formats
* Loading the provider alone does not access the storage and does not take over selection of built-in drivers
* If needed, HTTP tests that go through the actual `web` middleware and cookies, and locking for concurrent requests

## Related pages

> [card] [Session](https://kawax.biz/en/session.md)
>
> Review reading and writing sessions, flash data, and regenerating IDs.

> [card] [Manager internals](https://kawax.biz/en/advanced/manager.md)
>
> Review how custom drivers are created and retained, and how calls are delegated to the default driver.

> [card] [Custom cache stores in packages](https://kawax.biz/en/advanced/package-cache-drivers.md)
>
> Review the division of roles between the cache Store and Repository used as storage, and the TTL contract.

> [card] [Package service lifecycles](https://kawax.biz/en/advanced/package-service-lifetimes.md)
>
> Review dependencies retained by long-lived workers and how to isolate state.

## Primary sources

* [Official HTTP Session documentation: custom drivers and blocking](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/session.md)
* [Official package development documentation](https://github.com/laravel/docs/blob/625d1dfb61018e6bc50cc7ab67fca89ae018dfa2/packages.md)
* [SessionManager: custom creation and building the Store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionManager.php)
* [Store: loading, serialization, saving, and ID regeneration](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Store.php)
* [CacheBasedSessionHandler: TTL conversion and the storage contract](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/CacheBasedSessionHandler.php)
* [CacheManager: named stores and Redis configuration](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Manager: extend, driver, forgetDrivers](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Manager.php)
* [SessionServiceProvider: singleton registration of the Manager and session.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/SessionServiceProvider.php)
* [StartSession: saving, GC, and cache-based locking](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Session/Middleware/StartSession.php)
* [Official CacheBasedSessionHandlerTest: verifying missing data, TTL, and deletion](https://github.com/laravel/framework/blob/v13.39.0/tests/Session/CacheBasedSessionHandlerTest.php)


## Related topics

- [Core package and custom drivers - Feedable](https://kawax.biz/en/packages/feedable/core.md)
- [Custom cache stores and contracts in packages](https://kawax.biz/en/advanced/package-cache-drivers.md)
- [Custom Authentication Guards](https://kawax.biz/en/advanced/custom-auth-guard.md)
- [Custom agents](https://kawax.biz/en/packages/laravel-copilot-sdk/custom-agents.md)
- [Registering and re-resolving package Storage drivers](https://kawax.biz/en/advanced/package-filesystem-drivers.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.