This blog post series picks up where I left off in how AI agents revived my old PHP framework project.
In my previous post, I wrote about how a swarm of AI agents helped me revive a PHP framework I had abandoned years ago. That post was the origin story. This little series is the first real proof that the revival was worth it.
Because a framework on its own is just plumbing. What I really wanted to know was simpler and scarier: could I build a serious, production-grade feature on top of it without hating my life? Not another to-do list demo. The thing every app needs, nobody enjoys writing, and that ruins your week when you get it wrong.
Authentication.
So I built Polaris. It is the official identity module for Univeros, and this post is the tour. The four how-tos that follow get hands-on with each piece. Here I just want to show you the shape of the thing and why it is built the way it is.
I named it Polaris on purpose. Polaris is the fixed star sailors navigated by, and identity plays the same role in an application. Everything else eventually comes back to "Who is this, and what are they allowed to do?"
The Thirty-Second Version
Install it:
composer require univeros/polarisRegister it. This is the "one line" I keep bragging about:
// config/modules.php
return [
new Univeros\Polaris\Module(),
];Give it the secrets it needs (from your environment or a secret manager, never from a file in the repo):
export APP_KEY="…" # 32-byte base64, seeds the peppers + encrypter
export AUTH_JWT_PRIVATE_KEY="$(cat private.pem)" # signs access tokens
export AUTH_JWT_PUBLIC_KEY="$(cat public.pem)" # verification + the JWKS endpointRun the migrations and confirm it is alive:
bin/altair db:migrate
bin/altair routes:list --format=json | grep auth
bin/altair doctorThat is it. That single new Module() contributes every /auth, /users, and /orgs route, the Cycle ORM entities, the migrations, the authentication and authorization middleware, and the container bindings. Fifty-two endpoints, wired and ready, with zero per-module bootstrapping on your side.
Why a Module, and Why “Drop-In” Is More than a Buzzword
Here is something I learned the hard way over the years. Most auth code doesn’t die because the crypto is wrong, but because it is welded to the app that hosts it. You write a beautiful login flow for project A, then project B comes along and you spend two days surgically extracting it. By the time you’re done you’ve introduced three new bugs.
Univeros has a module contract system, and Polaris leans on it completely. A module declares what it provides (routes, entities, migrations, middleware, container bindings) and the host wires it all in automatically. There is no "now go register these fifteen services" step:
final class Module implements
ModuleInterface,
RoutesProviderInterface,
MiddlewareProviderInterface,
EntityDirectoriesProviderInterface,
MigrationDirectoriesProviderInterface
{
public function name(): string
{
return 'univeros/polaris';
}
public function apply(Container $container): void
{
$authConfig = AuthConfig::fromArray($this->authConfigArray());
$secrets = Secrets::fromEnvironment($this->environment());
$container->instance(AuthConfig::class, $authConfig);
$container->instance(Secrets::class, $secrets);
(new IdentityBindings())->apply($container);
(new TokenBindings())->apply($container, $authConfig, $secrets);
(new SessionBindings())->apply($container);
(new HttpBindings())->apply($container);
(new MfaBindings())->apply($container, $authConfig, $secrets);
(new OrganizationBindings())->apply($container);
}
}Notice that apply() builds the config and secrets eagerly. That is deliberate. Forget to set AUTH_JWT_PRIVATE_KEY, and the app does not boot with a quietly insecure fallback and bite you in production three weeks later. It fails immediately, with a clear message. Fail loud. Fail early.
The Shape of Every Endpoint
Before diving into the how-tos, there’s one pattern worth knowing. Once you see it, the rest of the module reads the same way. Univeros uses an Action, Input, Domain, Responder shape (it is basically Action-Domain-Responder with a typed input DTO in front). Every endpoint is the same quad:
HTTP edge Action thin route target: declares input, responder, domain, required permissions
Input (readonly) a typed request DTO with validation rules()
Responder turns a Payload into JSON or an RFC 9457 Problem Details body
Domain *Service the business logic: transactional, emits PSR-14 events
Contracts/* ports: SmsSender, OtpMailer, PasswordHasher, Clock, and friends
Persistence Entity/* (Cycle) UUID-v7 entities mapped into the host ORM schema
Security token machinery implements the framework's Altair\Http auth contractsThe domains stay thin. They validate the edge, call a service, and translate outcomes into HTTP. All the real work (hashing, lockout, token minting, event dispatch) lives in the services. That separation keeps the codebase from rotting, and it is why each how-to in this series can show you a real, unedited domain class that still fits on a single screen.
What You Actually Get
Security Posture, Stated Plainly
I am not going to wave my hands and say "secure by design." Here is what that phrase actually cashes out to:
- Passwords hashed with Argon2id, transparently rehashed when parameters change, with timing-equalized verification.
- Access tokens signed with asymmetric keys (RS256 or EdDSA) with kid-based rotation. Resource servers verify with the public key alone.
- Refresh tokens, OTP codes, recovery codes, verification tokens, and reset tokens are never stored in plaintext. They are hashed or kept as keyed HMACs.
- Rotating refresh tokens with family-based reuse detection.
- Rate limiting on every sensitive endpoint, sliding-window account lockout, and anti-enumeration on register, resend, and forgot-password.
- An audit log fed by the event stream.
The standards it follows are the boring, correct ones: JWT (RFC 7519), JWKS (RFC 7517), TOTP (RFC 6238), OAuth 2.0 refresh semantics and the Security BCP (RFC 9700), Problem Details (RFC 9457), and OWASP ASVS for password storage. The full threat model lives in the reference docs.
So, Did the Framework Hold Up?
That was the whole point of this exercise. Part one of the Univeros series asked whether AI agents could resurrect a dead PHP framework. This series is the honest test: could I build something genuinely hard on top of it?
I think the answer is yes, and the evidence is in the shape of the code. Polaris is fifty-two endpoints, full MFA, multi-tenant RBAC, rotating tokens with theft detection, and an audit trail, and yet every endpoint is the same readable quad. The framework's module system meant the whole thing installs in one line.
The agents helped a lot with the grind: the RFC 6238 test vectors, functional tests for fifty-two endpoints, keeping the docs in sync with code. But the design decisions (authority comes from the database, not the token; last-owner protection; fail-open breach checks) came from years of getting auth wrong and remembering the scars. The agents are fast hands. The judgment is still mine.
Where to Go Next
Pick the piece you need:
- Part 2: Logins that do not leak covers register, email verification, password login, JWT access tokens, and rotating refresh tokens with reuse detection (coming soon).
- Part 3: Real MFA in an afternoon covers TOTP/QR, SMS, email, recovery codes, the login gate, and step-up (coming soon).
- Part 4: One user, many orgs covers organizations, roles, permissions, the Gate, and the tenant invariants (coming soon).
- Part 5: Bring your own providers and events covers the SMS/email/breach ports and the PSR-14 event stream (coming soon).
The reference docs live at polaris.univeros.io. The framework is at univeros.io. The source is on GitHub.
composer require univeros/polarisOne line. A whole identity stack. The fixed star your app navigates by. See you in part 2.
Build captivating apps and sophisticated B2B platforms
Stunning solutions for web, mobile, or cross-platform applications.
Let's Talk%20-%20Featured%20Image%20for%20the%20Article.jpg)