mohamedhekal / oncegate
Stripe-style Idempotency-Key middleware for Laravel APIs with response replay and concurrent locks
Requires
- php: ^8.2
- illuminate/cache: ^11.0|^12.0
- illuminate/console: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/routing: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/phpstan: ^2.0
README
Stripe-style Idempotency-Key middleware for Laravel APIs. Store the first response, replay safe retries, reject payload mismatches, and return 409 while a request with the same key is still in flight.
Update badge URLs after publishing the GitHub repository.
Problem
Mobile apps, payment clients, and flaky networks retry POST/PUT requests. Without idempotency keys, those retries create duplicate orders, charges, or shipments.
Features
Idempotency-Keyheader handling (name configurable)- Request fingerprinting (method + path + body hash, optional user scope)
- Response replay with
Idempotent-Replayed: true - Fingerprint mismatch →
422 - In-flight conflict →
409 - Cache driver (default) and database driver
- Optional required-key mode
oncegate:purgefor expired database records
Requirements
- PHP 8.2+
- Laravel 11 or 12
Installation
composer require mohamedhekal/oncegate php artisan vendor:publish --tag=oncegate-config php artisan migrate
Quick start
use Illuminate\Support\Facades\Route; Route::middleware(['api', 'oncegate'])->group(function () { Route::post('/orders', [OrderController::class, 'store']); });
Client:
POST /orders HTTP/1.1 Idempotency-Key: 8e2f1a6c-9b0d-4c3e-a1f2-7d6c5b4a3210 Content-Type: application/json {"sku":"SKU-1","qty":1}
First response is executed normally. Retries with the same key and body receive the stored response and header Idempotent-Replayed: true.
Alias middleware name: idempotency (same class).
Configuration
| Key | Default | Meaning |
|---|---|---|
header |
Idempotency-Key |
Incoming header name |
methods |
POST, PUT, PATCH, DELETE | Methods that participate |
required |
false |
Abort 400 when header missing |
driver |
cache |
cache or database |
ttl |
86400 |
Stored response lifetime (seconds) |
lock_ttl |
60 |
Processing marker TTL (cache) |
fingerprint.include_user |
true |
Scope keys per authenticated user |
Architecture
See docs/architecture.md.
Testing
composer install
composer test
Security notes
- Keys are scoped by user when
fingerprint.include_useris enabled. - Do not log raw idempotency payloads if they contain secrets—pair with a redaction package if needed.
- Prefer Redis cache in production for atomic
addsemantics under load.
License
MIT — see LICENSE.
Credits
Mohamed Hekal.