ADR-0010 — Pluggable session storage backend via SessionHandlerFactory¶
Status: accepted (FT18 — 2026-05-26)
Context¶
NeNe uses PHP's default file-based session storage (session.save_handler = files). This works correctly for single-server deployments, but breaks immediately under a load-balancer with multiple application instances because each server maintains its own session directory. The issue was listed as the third commercial-viability concern in REPORT_commercial_feasibility.md.
Making session storage configurable requires three things:
- A PHP session handler (
SessionHandlerInterface) for the target backend. - A factory that reads a DSN environment variable and constructs the right handler.
- A hook in the bootstrap where the handler is registered before
session_start().
The FT13 Mailer pattern (one interface + one concrete implementation + one env-var + one Docker Compose service) is a close analogue for the design.
Decision¶
Introduce a pluggable session storage backend controlled by the NENE_SESSION_DSN environment variable.
New classes¶
Nene\Xion\RedisSessionHandler— implements PHP's nativeSessionHandlerInterface. Stores session data as plain strings under the Redis keysess_{id}with a TTL equal tosession.gc_maxlifetime. Redis handles key expiry natively;gc()is a deliberate no-op.Nene\Xion\SessionHandlerFactory— single entry point. ParsesSESSION_DSNand returns a\SessionHandlerInterfaceornull(→ PHP default file sessions). Currently supportsredis://host:port[/db].
Bootstrap hook¶
A new configureSessionHandler() function in htdocs/index.php is called between configureSessionCookie() and session_start(). When SESSION_DSN is empty, the function is a no-op — no behaviour change for existing deployments.
DSN convention¶
| DSN value | Backend |
|---|---|
| `` (empty) | PHP default file sessions |
redis://host:port |
Redis via RedisSessionHandler |
redis://host:port/db |
Redis DB N via RedisSessionHandler |
PHP client¶
predis/predis ^2.2 is added as a production dependency. It is pure PHP and requires no C extension in the Docker image. ext-redis remains an acceptable alternative for operators who can modify the Dockerfile; SessionHandlerFactory is intentionally decoupled from the client so the implementation can be swapped later.
Docker Compose¶
A redis:7-alpine service is added to compose.yaml with a healthcheck. The app service gains depends_on: redis: condition: service_healthy so the application never starts before Redis is ready. NENE_SESSION_DSN defaults to redis://redis:6379 in the compose environment, matching the Mailpit pattern (dev infrastructure available by default, replaced in production).
Environment variable¶
NENE_SESSION_DSN is read in ini/xSystemIni.php and exposed as the SESSION_DSN constant. Empty string = file sessions (no performance cost, no connection attempt).
Consequences¶
Positive:
- Multi-instance / load-balanced deployments now work out of the box with a single
NENE_SESSION_DSNenv change. - Single-server deployments are unaffected — empty DSN → no behaviour change.
- The factory pattern is open to additional backends (Memcached, DB-backed) without touching the bootstrap.
- Redis TTL handles session garbage collection natively; no
gc()logic needed.
Trade-offs:
predis/predisis a new production dependency. It is pure PHP and well-maintained, but adds ~500 KB to vendor.- Operators who need
ext-redis(for performance or cluster mode) must modify the Dockerfile and write a custom handler; the factory only ships apredis-backed implementation. - The compose default (
redis://redis:6379) means everydocker compose upnow starts a Redis container even for single-server use. Operators who want file sessions in Docker must explicitly setNENE_SESSION_DSN=in their.env.
Alternatives considered¶
Use ext-redis only¶
ext-redis would require adding pecl install redis && docker-php-ext-enable redis to the Dockerfile, making the Docker image larger and the build slower. predis/predis eliminates this and works identically for development use cases.
Modify ini_set('session.save_handler', 'redis') directly¶
PHP can be told to use the redis save handler via INI if ext-redis is installed. This is the fastest option in production but requires the C extension and is non-testable without a live Redis server. SessionHandlerInterface is testable with mocks.
Session handler in Initialize::init()¶
Centralising the handler registration in Initialize::init() would reduce the surface of htdocs/index.php, but Initialize::init() currently only loads INI files and does not interact with PHP session state. Keeping session configuration in the front controller alongside configureSessionCookie() is the more explicit and consistent choice.
Implementation tracking¶
- Trial: FT18 (
docs/field-trials/2026-05-field-trial-18.md) - feat PR: closes #428 (FT18 issue)