Passage is a Laravel package by Morcen Chavez that lets your app sit between a client and an external API, forwarding requests and responses while keeping full control over authentication, headers, and payload transformation — all through familiar Laravel routing and middleware.
The typical use case is when you need to call a third-party API from your frontend but don't want to expose API keys, need to normalize payloads, or want to enforce validation and auth logic in one place. Instead of building a custom proxy from scratch, Passage gives you a structured way to do it with minimal boilerplate.
Defining Routes
Routes are registered using the Passage facade, right alongside your regular routes. The {path?} wildcard captures any sub-path and forwards it upstream:
use Morcen\Passage\Facades\Passage; Passage::get('github/{path?}', GithubPassageController::class);Passage::post('stripe/{path?}', StripePassageController::class);Passage::any('payments/{path?}', PaymentsPassageController::class);
These support the standard HTTP methods — get, post, put, patch, delete, and any.
Creating a Handler
Each route points to a handler class that controls how requests are forwarded and responses are returned. Generate one with:
php artisan passage:controller GithubPassageController
Handlers extend PassageHandler and can implement three methods:
getOptions()— sets the upstream base URI and Guzzle options (timeouts, headers, etc.)getRequest()— transforms or injects credentials into the outgoing requestgetResponse()— transforms the upstream response before it reaches the client
A minimal handler might look like this:
class GithubPassageController extends PassageHandler{ public function getOptions(): array { return [ 'base_uri' => 'https://api.github.com/', ]; } public function getRequest(Request $request): Request { return $this->withBearerToken($request, config('services.github.token')); }}
Note the trailing slash on base_uri — it's required for path forwarding to work correctly.
Built-in Authentication Helpers
Passage ships with three authentication traits you can use inside getRequest():
- Bearer token —
$this->withBearerToken($request, $token) - API key (as a header or query param) —
$this->withApiKey($request, $key)or$this->withApiKeyQuery($request, $key, 'api_key') - HMAC signing —
$this->withHmacSignature($request, $secret)
You can also scaffold a handler with auth pre-wired:
php artisan passage:controller StripePassageController --with-auth=apikeyphp artisan passage:controller PaymentsPassageController --with-auth=hmac
Security
Passage automatically strips sensitive client headers — cookies, authorization, proxy-authorization — before forwarding requests upstream. If you need to selectively pass certain headers through (like forwarding the client's Authorization header), have your handler implement the AcceptsClientHeaders interface and define the allowedClientHeaders() method to return an allowlist:
class GithubPassageController extends PassageHandler implements AcceptsClientHeaders{ public function allowedClientHeaders(): array { return ['authorization']; }}
You can also restrict which hosts can be proxied by setting PASSAGE_ENFORCE_ALLOWED_HOSTS=true in your environment.
Inbound Validation and Resilience
Handlers can validate incoming requests before they ever reach the upstream service by implementing ValidatesInboundRequest. Define Laravel validation rules in a rules() method, and any failures return a 422 — no upstream call is made.
use Morcen\Passage\Contracts\ValidatesInboundRequest; class StripePassageController extends PassageHandler implements ValidatesInboundRequest{ public function getOptions(): array { return ['base_uri' => 'https://api.stripe.com/']; } public function rules(): array { return [ 'amount' => ['required', 'integer', 'min:1'], 'currency' => ['required', 'string', 'size:3'], ]; }}
For resilience, withRetry() adds automatic retry with exponential backoff:
class PaymentsPassageController extends PassageHandler{ public function getOptions(): array { return array_merge( ['base_uri' => 'https://payments.example.com/'], $this->withRetry(3, 200), // 3 retries, 200ms initial delay ); }}
Passage also supports response caching for GET/HEAD routes and streaming responses for large payloads.
Useful Artisan Commands
php artisan passage:list— lists all registered proxy routesphp artisan passage:health— checks connectivity to upstream services
You can also disable all proxying without touching your routes by setting PASSAGE_ENABLED=false in your .env.
Passage is a clean solution for scenarios where you need a lightweight API proxy inside an existing Laravel app without reaching for a full enterprise gateway. Learn more and explore the source code on GitHub.