Decisions: univeros/polaris 2.0
Append only, dated. The owner’s decisions of 2026-09-10 (the new major on polaris/core, polaris/psr15,
polaris/pdo and polaris/cli; PolarisMiddleware ahead of the framework’s exception handler; no
compatibility encrypter; one Cycle migration over Polaris\Pdo\SchemaInstaller; the HTTP contract frozen by
the 184 recorded fixtures) are recorded in polaris-core’s docs/adapters/univeros-polaris-2.0.md, the
hand-off this work applies. What follows is what was decided while applying it.
- 2026-09-11 · WP0 ·
1.xis a branch cut atv1.0.0(pushed before the first 2.0 change), so a 1.x fix has a home;maincarries 2.0. Work packages are branches named2.0/wpNfrom a freshmain, one PR each, squash-merged with the subject<type>: <description> (#N). - 2026-09-11 · WP0 ·
docs/auth/stays as the design reference (the data model, the flows, the MFA and RBAC rules, the API reference and the security model are the onespolaris/coreimplements, contract-frozen); its README says that the implementation ispolaris/coreand that only the module glue lives here. · Rejected: deleting the directory (the design has no other home in this repository and the 2.0 README links to it); rewriting it against thePolaris\namespaces (the same text lives inpolaris-core’sdocs/auth/, maintained there). - 2026-09-11 · WP0 ·
BearerTokenExtractor,NullCredentialsExtractorandUnauthorizedResponderstay in WP0, moved fromsrc/Http/Middleware/tosrc/Http/with their tests, rather than deleted and re-added in WP1: they did not move intopolaris/core(the hand-off lists them as kept from 1.0) and they depend on nothing that was deleted.src/is otherwise the placeholder module only. - 2026-09-11 · WP0 ·
composer qarunscs,stanandtest;test:contract(the functional suite through the module’s harness) is declared now and joinsqaand CI in WP3, whentests/Harness.phpexists.composer testalready runs thefunctionalsuite throughpolaris/core’s defaultPipelineHarness, which proves in WP0 that the vendored suite (thePolaris\Tests\autoload-dev mapping,polaris/testing,httpsoft/http-message, the fixtures) runs from this repository. · Rejected: aqathat fails until WP3. - 2026-09-11 · WP0 ·
migrations/exists from WP0 (a.gitkeep) sophpstan.neonandphpcs.xmlcan listsrc,testsandmigrationsbefore the migration lands in WP2. - 2026-09-11 · WP0 ·
.ai/skills/polaris/SKILL.md,README.md,CHANGELOG.mdandAGENT.mdstill describe 1.x until WP4, the documentation work package; WP0 changes no prose except this file and thedocs/authnote. - 2026-09-11 · WP1 · Nothing is built in
Module::apply(): every binding is a shared factory, so the onePolaris(and its PDO connection, manifest and JWT configuration) exists from the first request or command that resolves it.bin/altair db:migratetherefore boots on a host withoutAPP_KEYor the JWT keys, andpolaris:doctoris the boot check. · Rejected: building eagerly as 1.x did (fails every CLI command on a missing secret and opens a second database connection on every boot). - 2026-09-11 · WP1 · The config assembly and the connection live in two small classes,
Bootstrap\PolarisConfig::fromContainer()andDatabase\Connection(fromEnvironment(),fromSettings(),fromDsn(),databaseEnvironment()), not asModule::pdo()andModule::databaseEnvironment()static helpers as the hand-off sketched; the behaviour is the one described there andModulestays the binding list. The WP2 migration callsConnection::fromEnvironment().fromDsn()delegates toPolaris\Cli\Database::connect()(exception mode,PRAGMA foreign_keys = ONon SQLite), which thepolaris:*commands use too. - 2026-09-11 · WP1 · Every
Polaris\Wiring\Configport takes the container’s binding whenhas()says one is registered (mailer, SMS, breach check, cache, clock, dispatcher, logger, rate limits, rate store, encrypter, metrics, TOTP, QR codes, response factory), else null for core’s default; the database is a boundDatabaseAdapter, else a boundPDOwrapped inPdoAdapter, else a boundDatabaseSettings(whatCycleOrmConfigurationregisters), else the environment (DB_*first, thenPOLARIS_DSNwithPOLARIS_DB_USERandPOLARIS_DB_PASSWORD); thesqlserverdriver ofDatabaseSettingsis refused becausepolaris/pdohas no schema for it. - 2026-09-11 · WP1 · The environment is read through the framework’s
Altair\Configuration\Support\Env($_ENV,$_SERVER, thengetenv()), resolved from the container (Module::env()):EnvironmentConfigurationbindsEnvand loads.envwithDotenv::createImmutable()whenEnvis resolved, filling$_ENVand$_SERVERbut notgetenv(), so anew Env()orEnvironmentConfig::secrets()’sgetenv()default could read before the load or miss a.envkey. The default issuer withoutAUTH_ISSUERis core’spolaris(1.x minteduniveros/polaris); no token outlives the upgrade, the UPGRADE text will say so. - 2026-09-11 · WP1 ·
ResponseFactoryInterfaceis bound toLaminas\Diactoros\ResponseFactorywhen the host binds none (the skeleton binds none and builds its own), andIdentityValidatorInterfaceto aNullIdentityValidatorthat validates nothing: the framework’sTokenAuthenticationMiddlewarerequires one, and withNullCredentialsExtractorit never sees credentials, soPOST /auth/loginstays the sole credential entry point. · Rejected: a validator over Polaris’s password check (it would exist only to be bypassed). - 2026-09-11 · WP1 ·
TokenAuthenticationMiddlewareis bound as a shared autowired definition with theoptionsparameter (ssl => false,onError => UnauthorizedResponder) so the host gets the unscoped middleware withget()and a scoped one with$container->make(TokenAuthenticationMiddleware::class, ['rules' => [new RequestPathRule([...])]]), the framework’s own parameter mechanism. · Rejected: aPOLARIS_PROTECTED_PATHSvariable or a module setting (a second configuration surface for what the container already does). - 2026-09-11 · WP1 ·
PolarisMiddlewareserves only paths underPOLARIS_PATH_PREFIX:Polaris\Psr15\Routerstrips the prefix when present and otherwise matches the bare path, so without the guard a host mounting Polaris under/apiwould have its own/auth/...routes captured. The guard is the module’s, not a change to core (no entry inbehaviour-changes.md; the endpoints’ contract is untouched). - 2026-09-11 · WP1 · The module’s PSR-14 dispatcher (
Event\ListenerDispatcher) resolvesPolaris::listeners()from the container on the first dispatch (Polaris does not exist when its Config is assembled), sends every event to every listener (each Polaris listener ignores the events it does not handle) and honoursStoppableEventInterface. - 2026-09-11 · WP2 · The migration runs
SchemaInstalleron Cycle’s own connection (the protectedDriver::getPDO()through reflection), inside the transaction Cycle wraps aroundup()anddown(), not on a second connection as the hand-off sketched. Reproduced on a SQLite file: with the framework’squeryCache,Migrator::run()’sisConfigured()leaves thehasTablecursor open on Cycle’s connection, a SHARED lock that blocks any other connection’s commit (database is lockedafter the 60 s busy timeout); on the same connection,DROP TABLEstill fails withdatabase table is lockedwhile the migrator’s state cursor is pending, so the migration calls Cycle’s publicDriver::clearCache()first, which finalises the cached statements. One connection also makes the install atomic with PostgreSQL’s transactional DDL. · Rejected: WAL journal mode (the switch needs the same exclusive lock);Driver::disconnect()inside the transaction (Cycle then commits a transaction that no longer exists); aDatabaseAdapterover Cycle’sDatabaseInterface(a second adapter to maintain for one seeder call). - 2026-09-11 · WP2 ·
Console\Commands::all(Container)returns the sixpolaris/clicommands renamedpolaris:*on the module’s bindings: the connection callable isConnection::fromContainer()(a boundPDO, else the framework’sDatabaseSettings, else the environment), the secrets and auth callablesPolarisConfig::secrets()and::auth(), sopolaris:schema:createandpolaris:doctorrun on a host before Polaris itself can be built (no JWT keys yet) and on the database the application configured.univeros/clidiscovers#[Command]classes by directory and has no module hook, so the application’s ownbin/altairadds them (the demo in WP4 shows the file). - 2026-09-11 · WP2 · The migration test runs on the
DB_*database when the environment sets one (PostgreSQL in CI, the waydb:migrateruns there) and on a SQLite file otherwise; it discovers the module’s directory through the container tag asModuleMigrationDirectoriesdoes, applies through Cycle’sMigrator, proves parity withSchemaDiff, rolls back. - 2026-09-11 · WP3 ·
tests/Harness.phpimplementsPolaris\Tests\Functional\Harnessas the hand-off describes: anAltair\Container\Containerwith the test’s Config bound first (the database adapter, the secrets and auth settings, the cache asPolaris\Support\InMemoryCachewhen the test binds none, and every other non-null port),POLARIS_PATH_PREFIXfrom the Config’s prefix, thenModuleConfiguration([new Module()]); the request serialised to JSON bytes with an empty parsed body (whatServerRequestFactory::fromGlobals()produces); the skeleton’s Relay pipeline throughModuleMiddleware::collect()withExceptionHandlerMiddleware(capturing,ProblemDetailsErrorHandler), aDispatcherMiddlewareover an empty FastRoute table andActionMiddleware, resolved by the framework’sContainerResolver;transportHeaders()empty because Relay adds nothing. The 184 fixtures and the coverage test passed unchanged on the first run (185 tests, SQLite), so no difference was found and nothing is added tobehaviour-changes.md.qanow ends withtest:contract, and CI runs it as its own step aftercomposer test. - 2026-09-11 · WP4 ·
examples/univerosis theuniveros/univeros2.5.1 skeleton (composer create-project, inspected in a scratch directory first) withpublic/index.phpandconfig/container.phpunchanged and the module required from this repository through a path repository ("univeros/polaris": "@dev",../.., symlinked), so the demo and CI always run the checkout; the demo’scomposer.lockis not committed (it would pin a branch-specific dev reference of the module). The application’s ownApp\AppModulebinds the JSON-lines mailbox (var/mail.log) as Polaris’s mailer and anAppAuthenticationdecorator built from$container->make(TokenAuthenticationMiddleware::class, ['rules' => [new RequestPathRule(['path' => ['/app']])]]), contributed atDISPATCHER + 5;GET /app/mereads theDualTokenfrom theInputCollection(the framework’sInputParsermerges the request attributes) and answers with the user fromGraph::users(). · Rejected: a subclass ofTokenAuthenticationMiddlewarerepeating the module’s options; aPOLARIS_PROTECTED_PATHSvariable. - 2026-09-11 · WP4 ·
bin/setupwritesDB_DATABASEas an absolute path into.env: PHP’s built-in server runs withpublic/as its working directory, so the skeleton’s relativevar/polaris.sqliteresolved underpublic/and the first request failed with “unable to open database file”. The RS256 keys go into.envas double-quoted multi-line values (phpdotenv reads them) and intovar/keys/as PEM files;.envis loaded by the framework’sEnvironmentConfiguration(listed first inconfig/configurations.php, only when the file exists sobin/setupcan run before it does), andCycleOrmConfigurationshares theDB_*settings with Polaris.bin/altairbuildsconfig/container.php, appliesCliConfigurationover the framework’ssrc/Altair/*/Clidirectories and addsCommands::all(); itchdir()s to the project root sodb:migratefindsdatabase/migrations. - 2026-09-11 · WP4 ·
bin/walkthrough.shis a copy of polaris-core’sexamples/walkthrough.shplus two steps of this demo’s own (GET /app/mewithout a token answers 401 from the framework’s middleware, with the org-scoped token 200 with the organization id), so the demo proves the token bridge, not only the Polaris routes. ThedemoCI job installs the demo, runsbin/setupand the walkthrough on PHP 8.3. - 2026-09-11 · WP4 ·
phpcs.xmlalso coversexamples/univeros/{app,config,public}(the skeleton’sPingInputbraces reformatted with phpcbf); phpstan does not, because the demo’s classes autoload through the demo’s own vendor (univeros/framework), not this package’s. - 2026-09-11 · WP4 · Documentation:
README.mdrewritten for 2.0 (install,config/modules.php, the environment and the ports, the routes, the token bridge for the application’s routes,bin/altairand the migration, the demo, development),CHANGELOG.mdandUPGRADE.mdwith the hand-off’s text (plus the default-issuer note of WP1 and the application’s ownbin/altair),AGENT.mdand.ai/skills/polaris/SKILL.mdfor the 2.0 layout (the sections on the token model, the route table, the permission catalog, the tenant invariants and the integration flows kept, since the contract is unchanged). Every README ends with the 2am.tech line as in polaris-core. - 2026-09-11 · WP4 · Release:
v2.0.0is a GitHub release created at the merge commit of WP4 onmain(gh release create v2.0.0 --target <sha>) with the CHANGELOG’s 2.0.0 section as its notes; Packagist refreshes through its hook.