Tracking issue. Lands as a new sub-package at src/Altair/Idempotency/, registered via composer replace like the existing 37 sub-packages. Same CI, same release cadence, same .agent/packages/idempotency.md once bin/altair manifest:generate picks it up.
Why
Idempotency is a first-class concern for any API that gets retried by clients (mobile, queues, webhooks, agents). Stripe set the bar: clients pass Idempotency-Key, the server stores the (key, request fingerprint, response) tuple, and replays match future requests with the same key. PHP has nothing canonical here — every shop hand-rolls it badly, or ships without it and absorbs the duplicate writes.
Under the 2am.tech brief, this is a verified AI-era gap: agents retry. Without server-side idempotency, every agent integration is a footgun.
Grep-verified absent from univeros/framework as of 2026-05-30: zero Idempotency-Key middleware, zero job-handler decorator. The framework already ships rate-limiting, CSRF, auth (Basic/Digest/Token/JWT) and queue infrastructure (Messaging), but no idempotency layer.
Why as a sub-package (not a separate repo)
Same category as the existing cross-cutting HTTP concerns in src/Altair/Http/Middleware/ (rate-limit, CSRF, auth, sessions). All required deps already exist as sub-packages: Cache, Http, Messaging, Persistence. Living inside the monorepo means:
- Shows up in
bin/altair manifest:generate, .agent/packages/idempotency.md, Doctor, Introspection — discoverable by agents and humans.
- One coherent framework version via
composer replace.
- "Univeros ships idempotency built-in" is the marketing claim, not "compose Univeros + a third-party library."
Scope
PSR-only. No illuminate/*, no symfony/* runtime deps — pure PSR-7/15/16 plus the existing framework sub-packages.
Required surface
Altair\Idempotency\Middleware\IdempotencyMiddleware (PSR-15) — reads Idempotency-Key header, looks up store, returns cached response or wraps the handler.
Altair\Idempotency\Contracts\IdempotencyStoreInterface — find($key, $requestFingerprint), store($key, $request, $response, $ttl), lockAcquire($key, $ttl), lockRelease($key).
- Backends:
RedisIdempotencyStore (ext-redis or predis adapter, runtime choice)
PdoIdempotencyStore (Postgres + MySQL, schema migration shipped via Persistence / MigrationIntelligence)
InMemoryIdempotencyStore (testing)
Altair\Idempotency\Decorator\IdempotentHandler — queue-handler decorator usable from Messaging (Symfony Messenger bridge); contract lives here, bridge wiring lives in Messaging or in host config.
- Configurable: TTL (default 24h), header name (default
Idempotency-Key), fingerprinting strategy (method + path + body hash by default), response replay headers.
- Concurrent-request guard: lock acquire while first request is in flight; second request blocks or 409s — configurable.
- Streaming response handling (or explicit "not idempotent" sentinel for stream bodies).
Out of scope
- No client-side helper (Stripe SDK pattern is fine; this is server-side).
- No key generator — clients pick them.
Acceptance
Reference reading
Why
Idempotency is a first-class concern for any API that gets retried by clients (mobile, queues, webhooks, agents). Stripe set the bar: clients pass
Idempotency-Key, the server stores the (key, request fingerprint, response) tuple, and replays match future requests with the same key. PHP has nothing canonical here — every shop hand-rolls it badly, or ships without it and absorbs the duplicate writes.Under the 2am.tech brief, this is a verified AI-era gap: agents retry. Without server-side idempotency, every agent integration is a footgun.
Grep-verified absent from
univeros/frameworkas of 2026-05-30: zeroIdempotency-Keymiddleware, zero job-handler decorator. The framework already ships rate-limiting, CSRF, auth (Basic/Digest/Token/JWT) and queue infrastructure (Messaging), but no idempotency layer.Why as a sub-package (not a separate repo)
Same category as the existing cross-cutting HTTP concerns in src/Altair/Http/Middleware/ (rate-limit, CSRF, auth, sessions). All required deps already exist as sub-packages:
Cache,Http,Messaging,Persistence. Living inside the monorepo means:bin/altair manifest:generate,.agent/packages/idempotency.md,Doctor,Introspection— discoverable by agents and humans.composer replace.Scope
PSR-only. No
illuminate/*, nosymfony/*runtime deps — pure PSR-7/15/16 plus the existing framework sub-packages.Required surface
Altair\Idempotency\Middleware\IdempotencyMiddleware(PSR-15) — readsIdempotency-Keyheader, looks up store, returns cached response or wraps the handler.Altair\Idempotency\Contracts\IdempotencyStoreInterface—find($key, $requestFingerprint),store($key, $request, $response, $ttl),lockAcquire($key, $ttl),lockRelease($key).RedisIdempotencyStore(ext-redis or predis adapter, runtime choice)PdoIdempotencyStore(Postgres + MySQL, schema migration shipped viaPersistence/MigrationIntelligence)InMemoryIdempotencyStore(testing)Altair\Idempotency\Decorator\IdempotentHandler— queue-handler decorator usable fromMessaging(Symfony Messenger bridge); contract lives here, bridge wiring lives inMessagingor in host config.Idempotency-Key), fingerprinting strategy (method + path + body hash by default), response replay headers.Out of scope
Acceptance
src/Altair/Idempotency/exists withcomposer.json(PSR-4Altair\\Idempotency\\), follows the layout of existing sub-packages.composer.jsonreplaceblock asuniveros/idempotency.docs/packages/idempotency.mdshipped (per the docs-per-package rule).composer cs && composer stan && composer rectorclean.tests/Idempotency/).bin/altair manifest:generatepicks up.agent/packages/idempotency.mdautomatically.Reference reading
draft-ietf-httpapi-idempotency-key-header