Requirements
Requirements
These are taken directly from the package's composer.json — the resolver
enforces them, so this page only explains them.
Runtime
| Requirement | Version | Why |
|---|---|---|
| PHP | ^8.4 |
Uses PHP 8.4 language features throughout. |
| ext-openssl | * | RSA/EC key generation and signing (JWT RS256/ES256, SAML). |
| ext-sodium | * | Ed25519 signing and XChaCha20-Poly1305 AEAD for secrets at rest. |
Framework
| Requirement | Version |
|---|---|
Laravel (illuminate/*) |
^12.0 || ^13.0 |
The package is framework-native: it registers Cbox\Id\IdServiceProvider via
package auto-discovery and publishes migrations and config through it.
Composer dependencies
Pulled in automatically by composer require cboxdk/laravel-id:
| Package | Version | Used for |
|---|---|---|
cboxdk/laravel-ssrf |
^1.0 |
SSRF guard + DNS pinning for outbound webhooks. |
firebase/php-jwt |
^7.0 |
JWT signing/verification (access tokens, DPoP, id_token). |
onelogin/php-saml |
^4.0 |
SAML 2.0 response validation (signatures, XSW/XXE defense). |
spomky-labs/cbor-php |
^3.0 |
CBOR decoding for WebAuthn/passkey attestation. |
cboxdk/laravel-siem |
^0.1 |
Delivery engine for SIEM audit streaming. |
cboxdk/siem |
^0.1 |
SIEM payload formats (Splunk HEC, ECS, GELF, CEF). |
robrichards/xmlseclibs |
^3.1.5 |
XML-DSig signing for SAML IdP assertions and metadata. |
Storage
A relational database. Migrations use Laravel's standard schema builder and
Eloquent — but "standard schema builder" is not the same as "runs anywhere". For
sixty releases this page claimed four engines while the migrations could not create
a single table on MySQL (a json column cannot take a literal DEFAULT, error
1101). So this table now says only what CI proves:
| Engine | Migrations up | Migrations down | Full test suite |
|---|---|---|---|
| SQLite (in-memory) | ✅ every CI run | ✅ every CI run | ✅ every CI run — 1358 passed, 1 skipped |
| MySQL 8.0.13+ | ✅ every CI run (engines job, mysql:8) |
✅ every CI run | ✅ every CI run — 1386 passed |
| PostgreSQL 14+ | ✅ every CI run (engines job, postgres:16) |
✅ every CI run | ✅ every CI run — 1386 passed |
| MariaDB 10.2+ | ✅ every CI run (engines job, mariadb:11) |
✅ every CI run | ✅ every CI run — 1386 passed |
| SQL Server | ❌ never run | ❌ never run | ❌ never run |
| Others (Oracle, …) | Not supported. |
These rows were briefly marked "not currently proven", and that was the truthful reading at the time: the
enginesjob had never once connected. Its service containers started and reported healthy, but the job ran on a self-hosted runner that mounts the host's Docker socket, so the published port landed on the host while the job ran in a container on neither that loopback nor the service network. Every run failed withSQLSTATE[HY000] [2002] Connection refusedwhile this page reported ✅ — the same defect the preamble above describes, reached a second time by a different route.The job now runs on a GitHub-hosted runner, where service containers work, and the numbers above are from a real run against real servers.
The MySQL and MariaDB floors are the releases that introduced expression column
defaults (DEFAULT (json_object())); nothing older can express a default on a json
column at all, so the schema simply will not build there.
down() is now covered, and was not before
That column is new, and it was added the honest way. For sixty releases the engines
job carried a step captioned "migrations up and down" that rolled back nothing:
Testbench's rollback is scoped by --path to the one publishable migration the
TestCase loads directly, so everything the service provider registers went up and never
came back down. Measured on postgres:16 against an empty database, 83 tables and 90
rows in migrations survived the step. Nothing noticed, because migrate:rollback
exits 0 whether it reversed the whole schema or a single file and nothing looked at the
database afterwards.
tests/Migrations/MigrationRollbackTest.php is the replacement. It migrates every
registered path into a throwaway database, runs migrate:reset across all of them, and
requires zero tables and zero migrations rows before migrating back up again.
The assertion is the point; the rollback on its own proves nothing. It runs on all four
engines, including MariaDB.
It found five migrations whose down() was broken on first run. One left a stray
password_reset_tokens table behind after a full reset — the same name whose collision
with Laravel's skeleton migration broke every greenfield install in v0.62.0, so a
rolled-back database could not be migrated forward again.
One migration is deliberately irreversible and documented as such:
access-control/2026_07_24_000300_backfill_manual_permission_environments is a data
backfill that cannot distinguish an environment_id it set from one already present,
so reverting would risk nulling legitimately-scoped rows.
Both engines that were not green are green now
Two defects each took a whole engine out of CI. Both were found by this job, both are fixed, and both are kept here because the shape of each is worth knowing before you add a migration or a concurrent write path.
MariaDB — first-append deadlock on an empty audit chain. Eight processes appending
to a chain with no rows yet had no anchor to serialise on, so the FOR UPDATE looking
for it matched nothing — which InnoDB answers with a gap lock on the
(environment_id, scope, sequence) unique key. MariaDB resolved the pile-up as
SQLSTATE 40001 (error 1213, deadlock) rather than the duplicate-key error
DatabaseAuditLog::record() is written to absorb, and three attempts is not enough for
eight contenders on one gap: 6 of 800 appends failed, loudly rather than silently.
Fixed by finding the anchor with a plain read and locking it by primary key — an
exact primary-key match takes a record lock and can never take a gap lock — with the
search deliberately outside the transaction, because under REPEATABLE READ a
consistent read inside it fixes the snapshot the head read is then answered from. That
second half is not obvious from the documentation; an earlier attempt that kept the
lookup inside the transaction made it strictly worse. Now 1386/1386 on mariadb:11,
and 800/800 on the forking test.
PostgreSQL is green as of v0.62.0, and was not before: $table->ulid() compiled to
char(26), which PostgreSQL blank-pads, so every id shorter than 26 characters came
back to PHP padded and failed strict comparison — 338 of 1358 tests. Every identifier
column is varchar now. See Upgrading for the migration, and the
note below before you add a column.
SQL Server is not supported. It has never been run, by CI or by hand. The schema uses nothing knowingly incompatible with it, but "nothing knowingly incompatible" is exactly what this page used to say about MySQL.
Entitlement and session hot-paths benefit from a cache store (Redis recommended in production) but do not require one.
Portability constraints the schema is written against
Worth knowing before you add a migration to this package:
- A
jsoncolumn cannot carry a literal default on MySQL. Only the parenthesised expression form,DEFAULT (json_object()), is accepted. (MariaDB'sjsonis reallylongtext+ ajson_valid()CHECK and does take the literal — but it takes the expression too, so one spelling serves both.) UseCbox\Id\Kernel\Database\JsonDefault::emptyObject()/::emptyArray(), which picks per driver and leaves the PostgreSQL and SQLite DDL untouched. - InnoDB caps an index key at 3072 bytes, and utf8mb4 charges 4 bytes per
character — so a composite index over four
string()columns is already over the limit at the framework'svarchar(255)default. Give columns that sit in a composite index an explicit length. - Index and constraint names must fit in 63 characters — Postgres's limit, one tighter than MySQL's 64. Postgres truncates silently rather than failing, so an over-long generated name means the same index is called different things on different engines. Name long composite indexes explicitly.
- Never declare a
CHARcolumn — notchar(), and not theulid()/uuid()/foreignUlid()/ulidMorphs()helpers that compile to one. PostgreSQL implementsCHARasbpchar, which blank-pads every value out to the declared width and hands the padding to PHP through PDO.$row->environment_id === 'env_test'is then false for a row whoseenvironment_idisenv_test— while Postgres' own=andlength()strip the blanks, so the padding is invisible from a SQL client. Usestring($column, 26):varchardoes not pad on any supported engine.tests/Feature/SchemaPortabilityTest.phpfails the build if aCHARhelper reappears, and it runs on sqlite so it catches it on the first local run rather than in the server-engine job.