Skip to main content
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. For automatic provider loading, see Package discovery.

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. 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. 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.
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.
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.
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.
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.
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. 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, 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. 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, 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.
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

Session

Review reading and writing sessions, flash data, and regenerating IDs.

Manager internals

Review how custom drivers are created and retained, and how calls are delegated to the default driver.

Custom cache stores in packages

Review the division of roles between the cache Store and Repository used as storage, and the TTL contract.

Package service lifecycles

Review dependencies retained by long-lived workers and how to isolate state.

Primary sources

Last modified on November 2, 2026