Search by

enlivenapp / flight-sessions

Database-backed sessions for FlightPHP — encrypted payloads, unified API, AJAX-friendly

Maintainers

Package info

github.com/enlivenapp/flight-sessions

Type:flightphp-foundation

pkg:composer/enlivenapp/flight-sessions

Transparency log

Statistics

Installs: 63

Dependents: 2

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.3 2026-09-01 03:34 UTC

This package is auto-updated.

Last update: 2026-09-01 03:34:41 UTC


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, and last_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; Secure follows flight.force_https or 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:gc CLI 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 bundled sessions table 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.