enlivenapp / flight-sessions
Database-backed sessions for FlightPHP — encrypted payloads, unified API, AJAX-friendly
Package info
github.com/enlivenapp/flight-sessions
Type:flightphp-foundation
pkg:composer/enlivenapp/flight-sessions
Requires
- php: >=8.1
- ext-openssl: *
- ext-pdo: *
- flightphp/core: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- enlivenapp/migrations: Runs the bundled sessions table migration (php runway migrate:all)
Provides
None
Conflicts
None
Replaces
None
README
Database-backed session storage for FlightPHP. Sessions live in a SQL table through PHP's SessionHandlerInterface. Payloads are encrypted at rest with AES-256-GCM. A single SessionManager service handles cookie hardening, id regeneration, flash messages, and garbage collection for the whole request.
Features
- Database storage - one row per session, stamped on every write with
user_id, IP address, user agent, andlast_activity - Encryption at rest - AES-256-GCM (
enc1:prefix, random IV per write, verified auth tag); mandatory on web requests - No request locking - concurrent requests from the same client are not serialized (AJAX/HTMX friendly)
- Hardened cookies - HttpOnly, SameSite=Lax, strict mode;
Securefollowsflight.force_httpsor is set explicitly - Flash messages - two-generation scheme:
flash(),pullFlash(),hasFlash(),keepFlash() - Per-user session tools - active-session listings and remote logout via the handler API
- Garbage collection - probabilistic per request plus a deterministic
sessions:gcCLI command
Requirements
- PHP 8.1+
flightphp/core^3.0- MySQL or MariaDB (writes use MySQL upsert syntax)
- A PDO connection available as
$app->db() enlivenapp/migrations(recommended) to run the bundledsessionstable migration
Install
composer require enlivenapp/flight-sessions # Below recommended to run bundled migration # composer require enlivenapp/migrations # php runway migrate:all # creates the sessions table
The sessions table and its columns are defined in the bundled migration at src/Database/Migrations/. The migration runs through enlivenapp/migrations, which provides the php runway migrate:all runner. For setups that do not use that package, a raw SQL script with the same schema is at src/Database/sessions.sql.
Generate an encryption key and add it to .env:
php -r 'echo bin2hex(random_bytes(32));'
SESSION_ENCRYPTION_KEY=<64-character hex key>
Your app must call Plugin::register() and pass the app config directory as config_path:
use Enlivenapp\FlightSessions\Plugin; $plugin = new Plugin(); $plugin->register($app, $router, [ 'config_path' => '/path/to/app/config', ]);
Then you can override settings at app/config/sessions.php. Any key you return from that file wins over the defaults:
<?php // app/config/sessions.php return [ 'encryption_key' => 'your 64-character hex key', 'table' => 'custom_sessions', 'maxlifetime' => 3600, ];
How it works
Sessions live in SQL through PHP's SessionHandlerInterface, so $_SESSION and session_regenerate_id() keep working as normal. On every write the payload is encrypted and stored alongside the session id, user id, IP address, user agent, and timestamp. If a payload fails to decrypt, the row is deleted and the session restarts empty. One SessionManager instance owns the session for the request, so a second call to register() uses the same storage rather than starting a new one.
On web requests a missing key stops the application with HTTP 500 and a setup screen naming the env var. CLI commands run without a key. Rotating the key invalidates existing sessions: rows that no longer decrypt are deleted and their sessions restart empty.
Configuration
Defaults from src/Config/Config.php; override any key in app/config/sessions.php:
| Key | Default | Description |
|---|---|---|
table |
'sessions' |
Session table name |
cookie_name |
'flight_session' |
Session cookie name |
cookie_lifetime |
0 |
0 = browser-session cookie |
cookie_path |
'/' |
Cookie path |
cookie_domain |
'' |
Cookie domain |
cookie_secure |
null |
null follows flight.force_https; true/false overrides |
cookie_httponly |
true |
HttpOnly flag |
cookie_samesite |
'Lax' |
SameSite policy |
use_strict_mode |
true |
Reject uninitialized session ids |
maxlifetime |
7200 |
Idle timeout in seconds; drives GC deletes |
gc_probability / gc_divisor |
1 / 100 |
Roughly 1% chance of GC per request |
encryption_key |
'' |
64 hex chars; .env value takes precedence |
Usage
$session = $app->session(); // service bound by the plugin $session->set('key', $value); $value = $session->get('key', $fallback); $session->has('key'); $session->delete('key'); $value = $session->pull('once'); // read + remove $all = $session->all(); // everything except flash keys $session->clear(); // wipe data, keep session alive $session->regenerate(); // new id, data preserved - call on login $session->destroy(); // clear data, delete row, expire cookie $session->id(); // current session id or null $session->isActive();
Flash messages
A flash message is a value you store under a key, then read on the next request. The key is yours to pick, but you must use the same key to read it back. 'status' is just an example; you might use 'error' or 'notice'.
Writes go to the next generation and reads come from the current one; generations rotate once per start().
$session->flash('status', 'Your Flash Message'); // readable on the next request $status = $session->pullFlash('status'); // read once, then removed $session->keepFlash(); // carry current flash into next request
Per-user sessions
Bind the owning user so rows can be listed or revoked later:
$session->setUserContext($userId); // stamped onto every subsequent write
The handler exposes the query side. Each row contains session_id, ip_address, user_agent, and last_activity. String fields arrive HTML-escaped (htmlspecialchars, ENT_QUOTES, UTF-8), so they are safe to render directly; decode explicitly if you need raw values downstream:
use Enlivenapp\FlightSessions\Handlers\DatabaseHandler; $handler = new DatabaseHandler(\Flight::db(), 'sessions', $hexKey); $rows = $handler->findByUser($userId); // active sessions, newest first $count = $handler->destroyByUser($userId); // remote logout: deletes every row
Garbage collection
GC runs probabilistically (~1% of requests) and deletes rows idle longer than maxlifetime. For deterministic cleanup, schedule the CLI command:
php runway sessions:gc
Example cron entry:
*/15 * * * * cd /path/to/app && php runway sessions:gc
Plugin ordering
Load this plugin before anything that touches sessions. With the common enlivenapp plugin set, use priorities: sessions 2, flight-shield 5, flight-csrf 10. CSRF middleware must call its before() after plugins load so tokens persist through the same store.
Security notes
Payloads are encrypted with AES-256-GCM. Each payload carries its own random IV and auth tag, verified on read. If a payload fails to decrypt, the row is deleted and an empty session is returned, so tampered data never reaches application code. Session validity is possession of a valid session id plus a decryptable payload. The IP address and user agent are recorded for audit and listing only; they are never matched against incoming requests, since binding them causes false logouts on rotating IPs, shared NAT, and browser updates.
findByUser() HTML-escapes all string fields at the boundary, so request-controlled values such as a user agent cannot inject markup into admin screens that render them.
Cookies are HttpOnly with SameSite=Lax under strict mode; Secure follows flight.force_https. Call regenerate() on privilege changes such as login to prevent fixation. destroy() removes the row and expires the cookie.
License
MIT - see LICENSE.