storageless: PSR-7 sessions with no session storage at all
:mailbox_with_mail: storage-less PSR-7 session support
At a glance
- What is it?
- The psr7-sessions/storageless package provides PSR-7 and PSR-15 middleware that gives a stateless application a session, with the session data carried inside a signed token rather than written to a server-side store. The trade is explicit: no I/O and no superglobals, at the cost of a signing key that makes the whole scheme work or fail.
- Who is it for?
- Adopt storageless if you run a PSR-15 application such as a Mezzio service and you want sessions without a session store, particularly for long-running workers and asynchronous handlers where the superglobals-based approach does not work. Do not adopt it if you need to revoke a session on the server, since a stateless session cannot be invalidated without a denylist, which reintroduces the storage you were trying to remove.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The session lives in the token
PSR7Session is a PSR-7 and PSR-15 compatible middleware that enables session without I/O usage in PSR-7 based applications.
That phrase, without I/O usage, is the whole design. The conventional PHP session writes a file or a database row on every request and reads it back on the next one. This package does neither. The session data is carried in the cookie itself, in a signed token, so the server never looks anything up.
The consequences are symmetrical and worth stating before you choose it.
What you gain is that there is no session backend to run, scale or secure, and no session table to expire rows from. The readme makes the same point from the framework side: you can do this in asynchronous contexts and long-running processes, since no superglobals nor I/O are involved. That is the case that breaks the usual approach, because a long-running worker handles many requests in one process and the session state has to travel with the request rather than living in a global.
What you give up is server-side control. A session cannot be revoked, because there is nothing on the server to delete. The data in the cookie is visible to the client, in the sense that it is base64 or JWT encoded rather than encrypted, so anything sensitive does not belong in it. And the whole scheme rests on a single secret, since anyone knowing the signing key can forge any session.
The project is by ocramius, malukenho and lcobucci, which explains both the PSR-15 and the JWT parts of the design, and the licence is MIT.
Wiring it into a Mezzio application
Installation is a single Composer command:
composer require psr7-sessions/storagelessThe middleware is PSR7Sessions\Storageless\Http\SessionMiddleware, and the readme states that you can use it in any PSR-15 compatible middleware stack. In a mezzio/mezzio application it is piped into the application like this:
use Lcobucci\JWT\Configuration as JwtConfig;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key\InMemory;
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
$app = new \Mezzio\Application(/* ... */);
$app->pipe(new SessionMiddleware(
StoragelessConfig::fromJwtConfiguration(
JwtConfig::forSymmetricSigner(
new Signer\Hmac\Sha256(),
InMemory::base64Encoded((string) getenv('SESSION_SIGNING_KEY')), // see below on how to generate it
)
)
));Two things are visible there. The JWT side is lcobucci/jwt, with a symmetric HMAC SHA-256 signer and a key held in memory, base64 encoded, read from an environment variable. The package does not implement its own cryptography, it configures lcobucci's, which is the right division of labour and also why the readme points at the lcobucci documentation for the configuration object and the list of supported algorithms, including asymmetric keys.
Once the middleware is in the pipeline, the session is reached through the request attributes, which is the PSR-7 way of passing per-request state:
$app->get('/get', function (ServerRequestInterface $request, ResponseInterface $response) : ResponseInterface {
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
assert($session instanceof \PSR7Sessions\Storageless\Session\SessionInterface);
$session->set('counter', $session->get('counter', 0) + 1);
$response
->getBody()
->write('Counter Value: ' . $session->get('counter'));
return $response;
});The session object satisfies a SessionInterface, it has a get and a set, and it lives in the request rather than in a global.
The signing key is the whole security model
The readme spends a surprising amount of space on key handling, and the reason is that the security of the entire scheme is concentrated in one value.
Anyone knowing the signing key can forge any session, so the key must be kept secret, outside of the codebase, for example in an environment variable, and generated using a cryptographically secure pseudo-random number generator. The readme is emphatic: never use a key found in documentation or examples.
For the Sha256 signer above, the key can be generated with a single command:
php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'That is 32 random bytes, base64 encoded, which matches the InMemory::base64Encoded call in the middleware configuration.
There is a failure mode worth calling out, because it is the kind of thing that turns into a security incident later. An empty key is rejected, so a missing environment variable makes the configuration fail rather than silently using a weak key. That is the behaviour you want, and it means a deployment that forgets the variable will not start rather than start insecurely.
The related risk is operational rather than cryptographic. In a stateless design there is nothing to rotate but the key, and rotating it invalidates every session in flight, because every session was signed with the old one. For a service with a long session lifetime that is a real operational cost of key rotation, and it is the cost you are accepting in exchange for having no session store to expire.
The middleware is also usable with asymmetric keys, which changes the property. With a private key signing and a public key verifying, the application that verifies a session need not be able to create one, which matters if you split roles across services.
Binding the session to the client, and the reverse proxy trap
The optional feature here is client fingerprinting, and it exists to mitigate session hijacking, the risk of cookie theft giving an attacker a valid session.
You can bind the user session to its IP, from $_SERVER['REMOTE_ADDR'], and to the User-Agent, from $_SERVER['HTTP_USER_AGENT'], by enabling client fingerprinting:
use PSR7Sessions\Storageless\Http\SessionMiddleware;
use PSR7Sessions\Storageless\Http\Configuration as StoragelessConfig;
use PSR7Sessions\Storageless\Http\ClientFingerprint\Configuration as FingerprintConfig;
$app = new \Mezzio\Application(/* ... */);
$app->pipe(new SessionMiddleware(
StoragelessConfig::fromJwtConfiguration(/* ... */)
->withClientFingerprintConfiguration(
FingerprintConfig::forIpAndUserAgent()
)
));The failure behaviour is deliberately strict. When a request lacks any of the fingerprint sources, for example it has no User-Agent header, its session cookie is ignored and no session cookie is sent back to it, and such a client is served an empty session that is never persisted. The readme's diagnostic advice is direct: if no session ever persists, check that your server provides all the configured sources.
Behind a reverse proxy this becomes a trap, and the readme spends a paragraph on it. Headers such as X-Real-IP or X-Forwarded-For are sent by the client as well, so you must only trust them when the request comes from your own reverse proxy, and you must make sure the proxy overwrites them rather than appending to the client sent values. Otherwise an attacker who stole a session cookie can simply send the header with the victim's IP and defeat the fingerprint.
The documented fix is to write a custom implementation of the PSR7Sessions\Storageless\Http\ClientFingerprint\Source interface, so the extraction of the client IP comes from a source of truth you control rather than from a header the client can also set. The readme links an article on retrieving the client IP from a different source of truth for the general case.
The quality gates a maintainer runs on every change
The repository is a small PHP package with an unusually thorough set of checks, and the Makefile and the top level files describe them.
The default make target runs all the things, which is unit tests, code style, static analysis and coverage. The unit target runs PHPUnit with a coverage text summary, the cs target runs phpcbf and then phpcs, and the static-analysis target runs PHPStan with a 256M memory limit. There is also a bc-check target that checks for backwards compatibility breaks by requiring roave/backward-compatibility-check into a temporary directory and running it, and the coverage target runs Infection, the mutation testing tool, with a static analysis tool argument.
The mutation testing badge and the type coverage badge in the readme confirm that both are actually part of the workflow, not aspirational. Infection in particular is a strong signal, because it only passes if the test suite actually detects behavioural changes rather than merely executing lines.
There is a bc-check target because this is a 10.x package. The default branch is 10.3.x, and the recent releases are 10.3.0 from 2026-10-12, 10.2.1 from 2026-10-10 and 10.2.0 from 2026-05-25, so the maintainer is shipping minor releases frequently and has tooling to catch breaking changes before they land.
The whole toolchain runs in Docker. The Makefile uses docker compose run --rm php outside CI and skips Docker entirely when the CI variable is set, and the compose file builds the image from the Dockerfile with the project directory mounted and the working directory set to it. The Dockerfile is based on php:8.5, installs the pcov extension through the mlocati extension installer, and creates a non-root user whose uid and gid come from build arguments taken from the host, so files written in the container belong to you rather than to root.
The remaining top level files are the usual suspects for a package with this level of rigour: phpstan.neon with a baseline file, phpcs.xml.dist, phpunit.xml.dist, infection.json.dist, renovate.json for dependency updates, and a Dockerfile and compose file for the development environment.
Editorial conclusion
Adopt storageless if you run a PSR-15 application such as a Mezzio service and you want sessions without a session store, particularly for long-running workers and asynchronous handlers where the superglobals-based approach does not work. Do not adopt it if you need to revoke a session on the server, since a stateless session cannot be invalidated without a denylist, which reintroduces the storage you were trying to remove. Before you commit, generate the signing key with a CSPRNG and keep it in an environment variable, decide whether to enable client fingerprinting, and check the backwards compatibility check the project runs, because this is a 10.x line that breaks across major versions.
Frequently asked questions
What does the storageless package do?
It provides PSR-7 and PSR-15 compatible middleware that enables sessions with no I/O usage, so session data is carried in a signed token rather than written to a server-side store. That means no superglobals and no I/O, which also makes it usable in asynchronous contexts and long-running processes.
How do I install storageless?
With composer require psr7-sessions/storageless. The middleware is PSR7Sessions\Storageless\Http\SessionMiddleware and it can be used in any PSR-15 compatible middleware stack, for example a mezzio/mezzio application.
How do I generate the session signing key?
For the Sha256 signer, with php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'. The key must be kept secret outside the codebase, generated with a cryptographically secure pseudo-random number generator, and never taken from documentation or examples. An empty key is rejected so a missing environment variable fails the configuration rather than silently using a weak key.
What does client fingerprinting do in storageless?
It binds the session to the client IP and User-Agent to mitigate session hijacking. A request that lacks any configured source, such as one with no User-Agent header, has its session cookie ignored and is served an empty session that is never persisted. Behind a reverse proxy you need a custom ClientFingerprint\Source implementation, since headers such as X-Forwarded-For can be set by the client.
How is the storageless package tested and checked?
Through a Dockerised toolchain driven by make: PHPUnit for unit tests with a coverage summary, phpcs and phpcbf for code style, PHPStan for static analysis, roave/backward-compatibility-check for breaking changes and Infection for mutation testing, with separate targets for each and a default target that runs them all.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/psr7-sessions-storageless)Was this useful?