PHPackages                             morcen/passage - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. morcen/passage

ActiveLibrary[API Development](/categories/api)

morcen/passage
==============

API gateway for Laravel

v3.0.0(1mo ago)6265[1 issues](https://github.com/morcen/passage/issues)[6 PRs](https://github.com/morcen/passage/pulls)MITPHPPHP ^8.2CI passing

Since Mar 12Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/morcen/passage)[ Packagist](https://packagist.org/packages/morcen/passage)[ Docs](https://github.com/morcen/passage)[ RSS](/packages/morcen-passage/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (8)Dependencies (21)Versions (24)Used By (0)

Passage: Lightweight API proxy gateway for Laravel
==================================================

[](#passage-lightweight-api-proxy-gateway-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/1e9459afbee3decccab1c6b7d67d691cd419c3ae62175d18976a2f726e6d28e4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6f7263656e2f706173736167652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/morcen/passage)[![GitHub Tests Action Status](https://camo.githubusercontent.com/1ce9a212137a19b873141d36fd916f764e30c8b4a9e5d299cb47307f8d6eb966/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d6f7263656e2f706173736167652f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/morcen/passage/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/aff1c9771688e835aef1cf9ec88e0f04235f62e34c82a09b9c79848a71437505/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d6f7263656e2f706173736167652f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/morcen/passage/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/200f83cf5596f2fb58870bc13345b284ed12f99eef60ae789ef771a33ad1df0a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d6f7263656e2f706173736167652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/morcen/passage)

Introduction
------------

[](#introduction)

Passage is a lightweight API gateway package for Laravel that proxies incoming requests to external services. It gives you per-route control over HTTP method, path, request transformation, and response transformation — using a routing syntax that mirrors Laravel's own.

Why developers use Passage
--------------------------

[](#why-developers-use-passage)

Passage is for Laravel apps that need to sit in front of one or more external APIs and expose them through your own application routes.

It is especially useful when you want to:

- Keep frontend or client apps talking to your Laravel app instead of directly to third-party APIs
- Centralize headers, tokens, request shaping, and response shaping in one place
- Add Laravel middleware, route groups, authentication, or rate limiting around upstream API calls
- Hide upstream API structure from consumers so you can change providers later with less surface-area impact
- Build a thin backend-for-frontend layer without writing the same HTTP plumbing over and over

In practice, Passage helps when your app needs a controlled proxy layer, not a full API management platform. You define proxy routes like normal Laravel routes, then customize how each request is forwarded and how each upstream response is returned.

### Passage is a good fit when

[](#passage-is-a-good-fit-when)

- You need a simple API proxy inside an existing Laravel app
- You want route-level control over how requests are forwarded
- You need to inject auth credentials or normalize payloads before calling an upstream service
- You want to reuse Laravel's routing and middleware system instead of introducing a separate gateway product

### Passage is probably not the right fit when

[](#passage-is-probably-not-the-right-fit-when)

- Your app calls external APIs only from internal service classes or jobs and does not need inbound proxy routes
- You need a full enterprise API gateway with dashboards, analytics, service discovery, advanced policies, or traffic orchestration
- You need complex multi-service aggregation, retries, circuit breakers, or workflow logic as a first-class feature
- You want a general-purpose HTTP client wrapper rather than a request proxy layer

If you are building a Laravel app that needs to expose a stable, app-owned endpoint in front of external APIs, Passage gives you a lightweight and Laravel-native way to do that.

If you want to see how this maps to real projects, read the [example scenarios](example-scenarios/README.md).

Features
--------

[](#features)

- Route-based proxy definitions using a familiar `Passage::get/post/...` API
- Per-route request and response transformation hooks
- Global and per-handler Guzzle options (timeout, headers, etc.)
- Works naturally with Laravel route groups, middleware, named routes, and `route:list`
- Secure by default: sensitive client headers stripped before forwarding
- Auth helper traits: Bearer token, API key, HMAC signing
- Inbound request validation via Laravel's built-in validator
- Response caching for GET/HEAD routes
- Automatic retry with configurable backoff
- Streaming response support for large payloads
- Laravel event hooks around every proxy call
- Connectivity health check via `passage:health`

Requirements
------------

[](#requirements)

- PHP 8.2 or higher
- Laravel 11.x or 12.x

> **Upgrading from v2?**v3.0.0 is a breaking release. The config-based `services` array and `Route::passage()` macro have been removed. See the [Upgrading from v2](#upgrading-from-v2) section below.

> **On v1.x?**PHP 8.1 and Laravel 10.x are no longer supported as of v2.0.0. Use [v1.2.4](https://github.com/morcen/passage/releases/tag/v1.2.4) for older environments.

Installation
------------

[](#installation)

```
composer require morcen/passage
```

Then publish the config file:

```
php artisan passage:install
```

To publish the controller stub for generating Passage handlers:

```
php artisan vendor:publish --tag=passage-stubs
```

Usage
-----

[](#usage)

### Defining proxy routes

[](#defining-proxy-routes)

Passage routes are defined in your route files (e.g. `routes/web.php`) using the `Passage` facade. The syntax mirrors Laravel's own routing:

```
use Morcen\Passage\Facades\Passage;

Passage::get('github/{path?}', GithubPassageController::class);
Passage::post('stripe/{path?}', StripePassageController::class);
Passage::any('payments/{path?}', PaymentsPassageController::class);
```

Each call registers a real Laravel route, so your proxy routes appear in `php artisan route:list` alongside your application's own routes.

The `{path?}` parameter captures the sub-path that is forwarded to the upstream service. For example:

```
GET /github/users/morcen  →  GET https://api.github.com/users/morcen
POST /stripe/charges      →  POST https://api.stripe.com/charges

```

All supported methods: `get`, `post`, `put`, `patch`, `delete`, `any`.

### Route groups

[](#route-groups)

Passage routes work inside any Laravel route group:

```
Route::prefix('v1')->middleware('auth')->group(function () {
    Passage::get('github/{path?}', GithubPassageController::class);
    Passage::post('stripe/{path?}', StripePassageController::class);
});
```

Named routes and other route chaining also work:

```
Passage::get('github/{path?}', GithubPassageController::class)
    ->name('github.proxy')
    ->middleware('throttle:60,1');
```

### Creating a Passage handler

[](#creating-a-passage-handler)

Every Passage route requires a handler class. Generate one with:

```
php artisan passage:controller GithubPassageController
```

This creates `app/Http/Controllers/Passages/GithubPassageController.php` extending `PassageHandler`:

```
use Morcen\Passage\PassageHandler;

class GithubPassageController extends PassageHandler
{
    public function getOptions(): array
    {
        return [
            'base_uri' => 'https://api.github.com/',
        ];
    }
}
```

You only need to override the methods relevant to your handler. All three interface methods have no-op defaults in `PassageHandler`:

- `getOptions(): array` — upstream base URI and any Guzzle options
- `getRequest(Request $request): Request` — transform or add credentials before forwarding
- `getResponse(Request $request, Response $response): Response` — transform the upstream response

If you prefer to implement the interface directly without the base class, implement `PassageControllerInterface` instead.

> **Note:** The `base_uri` must end with a trailing slash `/`, otherwise sub-path forwarding may not work correctly.

### Global options

[](#global-options)

Timeout and connection settings that apply to all Passage routes can be configured in `config/passage.php` or via environment variables:

```
PASSAGE_TIMEOUT=30
PASSAGE_CONNECT_TIMEOUT=10
```

Options defined in a handler's `getOptions()` override these global defaults.

### Listing proxy routes

[](#listing-proxy-routes)

```
php artisan passage:list
```

Displays a table of all registered Passage routes with their HTTP methods, URIs, and upstream targets.

### Disabling Passage

[](#disabling-passage)

Set `PASSAGE_ENABLED=false` in your `.env` to disable all Passage proxying without removing route definitions:

```
PASSAGE_ENABLED=false
```

---

Security
--------

[](#security)

### Header stripping

[](#header-stripping)

Passage strips sensitive client-origin headers before forwarding requests upstream. By default, `cookie`, `authorization`, and `proxy-authorization` are removed from every incoming request. This prevents client credentials from leaking to upstream services.

Handlers can re-add credentials from your own config inside `getRequest()`:

```
public function getRequest(Request $request): Request
{
    $request->headers->set('Authorization', 'Bearer '.config('services.github.token'));
    return $request;
}
```

To change which headers are stripped globally, edit `config/passage.php`:

```
'security' => [
    'strip_client_headers' => ['cookie', 'authorization', 'proxy-authorization'],
],
```

### Forwarding a client header on a specific route

[](#forwarding-a-client-header-on-a-specific-route)

If a route legitimately needs to forward a specific client header (for example, forwarding a client's `Authorization` to an upstream that validates it), implement `AcceptsClientHeaders` on the handler:

```
use Morcen\Passage\Contracts\AcceptsClientHeaders;
use Morcen\Passage\PassageHandler;

class RelayHandler extends PassageHandler implements AcceptsClientHeaders
{
    public function allowedClientHeaders(): array
    {
        return ['authorization'];
    }

    public function getOptions(): array
    {
        return ['base_uri' => 'https://api.partner.com/'];
    }
}
```

The listed headers bypass the strip policy only for that handler. All other handlers continue to strip them.

### Allowed hosts guard

[](#allowed-hosts-guard)

To prevent a misconfigured handler from proxying to an unintended host, enable the allowed hosts guard:

```
PASSAGE_ENFORCE_ALLOWED_HOSTS=true
```

Then list the permitted upstream hostnames in `config/passage.php`:

```
'security' => [
    'enforce_allowed_hosts' => true,
    'allowed_hosts' => ['api.github.com', 'api.stripe.com'],
],
```

Any handler whose `base_uri` resolves to a host not in the list will throw `DisallowedProxyTargetException` instead of forwarding the request.

### Aborting a request from a handler

[](#aborting-a-request-from-a-handler)

To abort a request early with a specific HTTP status, throw `PassageRequestAbortedException` inside `getRequest()`:

```
use Morcen\Passage\Exceptions\PassageRequestAbortedException;

public function getRequest(Request $request): Request
{
    if (! $this->isAllowed($request)) {
        throw new PassageRequestAbortedException('Access denied.', 403);
    }
    return $request;
}
```

Passage catches this exception and returns a JSON error response with the given status code.

### Inbound validation

[](#inbound-validation)

To validate the incoming request before it is forwarded, implement `ValidatesInboundRequest` and declare Laravel validation rules:

```
use Morcen\Passage\Contracts\ValidatesInboundRequest;
use Morcen\Passage\PassageHandler;

class CreateOrderHandler extends PassageHandler implements ValidatesInboundRequest
{
    public function rules(): array
    {
        return [
            'product_id' => ['required', 'integer'],
            'quantity'   => ['required', 'integer', 'min:1'],
        ];
    }

    public function getOptions(): array
    {
        return ['base_uri' => 'https://orders.example.com/'];
    }
}
```

Validation runs before `getRequest()`. If it fails, a 422 response is returned and the upstream is never called.

### Rate limiting

[](#rate-limiting)

Passage routes are real Laravel routes, so the built-in `throttle` middleware works directly:

```
Passage::post('orders/{path?}', CreateOrderHandler::class)
    ->middleware('throttle:60,1');
```

---

Auth helpers
------------

[](#auth-helpers)

`PassageHandler` includes three built-in auth traits. Use them inside `getRequest()` to inject credentials:

### Bearer token

[](#bearer-token)

```
use Morcen\Passage\PassageHandler;

class GithubHandler extends PassageHandler
{
    public function getRequest(Request $request): Request
    {
        return $this->withBearerToken($request, config('services.github.token'));
    }

    public function getOptions(): array
    {
        return ['base_uri' => 'https://api.github.com/'];
    }
}
```

Generate a handler pre-scaffolded for Bearer auth:

```
php artisan passage:controller GithubHandler --with-auth=bearer
```

### API key

[](#api-key)

```
public function getRequest(Request $request): Request
{
    // Inject as a header (default: X-API-Key)
    return $this->withApiKey($request, config('services.stripe.key'));

    // Or inject as a query parameter
    return $this->withApiKeyQuery($request, config('services.stripe.key'), 'api_key');

    // Or use a custom header name
    return $this->withApiKey($request, config('services.stripe.key'), 'X-Stripe-Key');
}
```

```
php artisan passage:controller StripeHandler --with-auth=apikey
```

### HMAC signing

[](#hmac-signing)

```
public function getRequest(Request $request): Request
{
    return $this->withHmacSignature($request, config('services.partner.secret'));
}
```

This signs the request body and a timestamp using HMAC-SHA256 and adds `X-Timestamp` and `X-Signature` headers to the outgoing request.

```
php artisan passage:controller PartnerHandler --with-auth=hmac
```

---

Resilience
----------

[](#resilience)

### Retry

[](#retry)

Add automatic retry by returning `passage_retry_times` (and optionally `passage_retry_sleep_ms`) from `getOptions()`, or use the `withRetry()` helper from `HasResilienceOptions`:

```
use Morcen\Passage\PassageHandler;

class PaymentsHandler extends PassageHandler
{
    public function getOptions(): array
    {
        return array_merge(
            ['base_uri' => 'https://payments.example.com/'],
            $this->withRetry(3, 200),
        );
    }
}
```

```
php artisan passage:controller PaymentsHandler --with-retry
```

`withRetry($times, $sleepMs, ?callable $when)` accepts an optional third argument — a callable that receives the exception and response and returns `true` if the request should be retried:

```
$this->withRetry(
    times: 3,
    sleepMs: 200,
    when: function (\Exception $e, \Illuminate\Http\Client\Response $response) {
        // Only retry on connection errors, not on 4xx responses
        return $e instanceof \Illuminate\Http\Client\ConnectionException;
    }
)
```

### Upstream error handling

[](#upstream-error-handling)

Passage maps transport-layer failures to appropriate HTTP status codes automatically:

CauseStatusConnection refused / DNS failure502 Bad GatewayTimeout504 Gateway TimeoutToo many redirects502 Bad GatewayUnexpected exception500 Internal Server ErrorUpstream 4xx and 5xx responses are passed through unchanged.

---

Caching
-------

[](#caching)

GET and HEAD responses can be cached per-route. Return `passage_cache_ttl` (seconds) from `getOptions()`:

```
public function getOptions(): array
{
    return [
        'base_uri'          => 'https://api.example.com/',
        'passage_cache_ttl' => 60,
    ];
}
```

```
php artisan passage:controller ExampleHandler --with-cache
```

The cache store used defaults to Laravel's default cache driver. To use a specific store, set it in `config/passage.php`:

```
'cache' => [
    'store' => 'redis',
],
```

Or via environment variable:

```
PASSAGE_CACHE_STORE=redis
```

---

Streaming
---------

[](#streaming)

For large or long-running upstream responses, enable streaming so Passage does not buffer the full response body in memory:

```
public function getOptions(): array
{
    return [
        'base_uri'          => 'https://files.example.com/',
        'passage_streaming' => true,
    ];
}
```

When streaming is enabled, the `getResponse()` transformation hook is skipped (the response body has not been read yet). The Content-Type and other upstream headers are still passed through.

---

Observability
-------------

[](#observability)

### Events

[](#events)

Passage fires three Laravel events around every proxy call:

EventWhen`PassageRequestSending`Before the upstream call`PassageResponseReceived`After a successful response`PassageRequestFailed`After a transport errorTo log all Passage activity, register `PassageEventSubscriber` in your `EventServiceProvider`:

```
use Morcen\Passage\Listeners\PassageEventSubscriber;

protected $subscribe = [
    PassageEventSubscriber::class,
];
```

This subscriber logs to a `passage` channel at `info` level (request/response) and `error` level (failures).

To disable events:

```
PASSAGE_EVENTS=false
```

### Health check

[](#health-check)

Ping the `base_uri` of every registered Passage route and see connectivity status:

```
php artisan passage:health
```

Useful in CI pipelines and post-deployment checks. Use `--timeout=10` to adjust the per-route probe timeout (default: 5 seconds).

---

Production checklist
--------------------

[](#production-checklist)

Before deploying Passage in production:

- Set `PASSAGE_ENFORCE_ALLOWED_HOSTS=true` and list all permitted upstream hosts in `config/passage.php`
- Confirm that sensitive headers (`cookie`, `authorization`) are NOT being forwarded unless intentional (check `strip_client_headers`)
- Apply `throttle` middleware to any publicly accessible Passage routes
- Set `PASSAGE_TIMEOUT` and `PASSAGE_CONNECT_TIMEOUT` appropriate for your upstream services (default: 30s / 10s)
- Enable retry (`passage_retry_times`) for routes calling unreliable upstream services
- Enable caching (`passage_cache_ttl`) for high-traffic read-only routes
- Register `PassageEventSubscriber` and configure a `passage` log channel for observability
- Run `php artisan passage:health` as a post-deployment check

---

Upgrading from v2
-----------------

[](#upgrading-from-v2)

v3.0.0 is a **breaking release**. If you are on v2 and are not ready to migrate, pin your version in `composer.json`:

```
"morcen/passage": "^2.0"
```

### What changed

[](#what-changed)

v2v3`config/passage.php` `services` arrayRemoved — routes are defined in route files`Route::passage()` in `routes/web.php`Removed — use `Passage::get/post/...` insteadArray-based handlers (`['base_uri' => '...']`)Removed — a handler class is always required### Migration steps

[](#migration-steps)

**1. Remove `Route::passage()` from your route files.**

**2. For each entry in `config/passage.php` `services`:**

If the entry was an array:

```
// v2 config/passage.php
'github' => ['base_uri' => 'https://api.github.com/'],
```

Create a handler class (or use `passage:controller`) and move `base_uri` into `getOptions()`:

```
// v3 app/Http/Controllers/Passages/GithubPassageController.php
public function getOptions(): array
{
    return ['base_uri' => 'https://api.github.com/'];
}
```

If the entry was already a controller class, it can be reused as-is — just make sure it implements `PassageControllerInterface`.

**3. Register routes in your route files:**

```
// v3 routes/web.php
use Morcen\Passage\Facades\Passage;

Passage::get('github/{path?}', GithubPassageController::class);
```

**4. Remove the `services` key from `config/passage.php`** (or re-publish the config with `php artisan vendor:publish --tag=passage-config --force`).

---

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Morcen Chavez](https://github.com/morcen)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

49

—

FairBetter than 95% of packages

Maintenance88

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community15

Small or concentrated contributor base

Maturity67

Established project with proven stability

 Bus Factor1

Top contributor holds 77.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~186 days

Recently: every ~244 days

Total

7

Last Release

45d ago

Major Versions

v0.2.1 → v1.2.32023-08-06

v1.2.3 → v2.0.02025-08-23

v1.2.4 → v3.0.02026-04-03

PHP version history (2 changes)v0.1.0PHP ^8.1

v2.0.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/0d29181937219755d8ae3ceda87a04c1fe93478a4bdcfa347daef6499ec472e9?d=identicon)[morcen](/maintainers/morcen)

---

Top Contributors

[![morcen](https://avatars.githubusercontent.com/u/28680830?v=4)](https://github.com/morcen "morcen (98 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (17 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (7 commits)")[![abhijeetnardele24-hash](https://avatars.githubusercontent.com/u/234410808?v=4)](https://github.com/abhijeetnardele24-hash "abhijeetnardele24-hash (3 commits)")[![Adityasingh-upkgs](https://avatars.githubusercontent.com/u/271292120?v=4)](https://github.com/Adityasingh-upkgs "Adityasingh-upkgs (1 commits)")

---

Tags

api-gatewayapi-proxylaravellaravelapi-gatewayPassageapi-proxymorcen

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/morcen-passage/health.svg)

```
[![Health](https://phpackages.com/badges/morcen-passage/health.svg)](https://phpackages.com/packages/morcen-passage)
```

###  Alternatives

[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.0k7.8M57](/packages/dedoc-scramble)[scalar/laravel

Render your OpenAPI-based API reference

6183.9k2](/packages/scalar-laravel)[ryangjchandler/bearer

Minimalistic token-based authentication for Laravel API endpoints.

8129.8k](/packages/ryangjchandler-bearer)[combindma/laravel-facebook-pixel

Meta pixel integration for Laravel

4956.9k](/packages/combindma-laravel-facebook-pixel)[stechstudio/laravel-hubspot

A Laravel SDK for the HubSpot CRM Api

2971.0k](/packages/stechstudio-laravel-hubspot)[njoguamos/laravel-plausible

A laravel package for interacting with plausible analytics api.

208.8k](/packages/njoguamos-laravel-plausible)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
