---
title: "Middleware, rule and scopes — Sentinel for Laravel | Roundly"
description: "Verify route models with sentinel.verified, validate input with IntactSeal, check whole collections with verifySeals() and query by seal state."
url: https://roundly-consulting.com/open-source/docs/sentinel-for-laravel/middleware-and-rules
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sentinel for Laravel](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)

# Middleware, rule and scopes

| Alias | Class | Parameters | Behaviour |
| --- | --- | --- | --- |
| `sentinel.verified` | `Http\Middleware\VerifySeals` | none = every route model; or param\[@seal\],… | Verifies route models after route-model binding; a failure answers middleware.verified\_status (409) with a generic message — never the status or reason. |
| `sentinel.idempotent` | `Http\Middleware\EnsureIdempotency` | optional (default) or required, optional TTL seconds | Idempotency keys. |
| `sentinel.signed` | `Http\Middleware\VerifyHttpSignature` | optional profile name | HTTP message signatures. |
| `sentinel.single-use` | `Http\Middleware\ConsumeSingleUseUrl` | — | Single-use URLs. |

Recommended order: sentinel.signed → auth → sentinel.idempotent → (route-model bindings) → sentinel.verified.

## sentinel.verified

```php
Route::get('/invoices/{invoice}', ShowInvoice::class)->middleware('sentinel.verified');
Route::put('/invoices/{invoice}/lines/{line}', UpdateLine::class)->middleware('sentinel.verified:invoice@financial');
```

- It runs after route-model binding — declared as route middleware inside the web or api groups, it does. A named parameter without @seal verifies every seal of that model.
- A failure with middleware.verified\_reaction = abort (the default) throws SealVerificationFailedHttpException: status middleware.verified\_status (409) and a generic translated message — the status and reason are never revealed to the client. Its getPrevious() is the TamperedModelException, for your exception handler’s report().
- With report, the finding is reported (TamperDetected, log) and the request continues.
- A named parameter that is unbound or not sealable throws SealingMisconfiguredException (500, fail closed).

## Typed parameters

Each middleware has a static helper validated when the route is declared — a typo fails when the routes load, not at request time:

| Helper | Validates |
| --- | --- |
| `VerifySeals::using(string ...$parameters)` | Each ^\[A-Za-z0-9\_\]{1,64}(@\[a-z0-9\_.-\]{1,64})?$; none = every sealable parameter. |
| `EnsureIdempotency::optional(?int $ttl = null) / required(?int $ttl = null)` | TTL 60–2 592 000 seconds (the middleware’s own range). |
| `VerifyHttpSignature::profile(?string $profile = null)` | The profile exists under sentinel.signatures.profiles; null = the default profile. |

```php
use RoundlyConsulting\Sentinel\Http\Middleware\EnsureIdempotency;
use RoundlyConsulting\Sentinel\Http\Middleware\VerifyHttpSignature;
use RoundlyConsulting\Sentinel\Http\Middleware\VerifySeals;

Route::put('/invoices/{invoice}', UpdateInvoice::class)->middleware(VerifySeals::using('invoice@financial'));
Route::post('/orders', StoreOrder::class)->middleware(EnsureIdempotency::required(ttl: 3600));
Route::post('/partner/events', PartnerEvents::class)->middleware(VerifyHttpSignature::profile('partners'));
```

## IntactSeal and verifySeals()

```php
use RoundlyConsulting\Sentinel\Rules\IntactSeal;

$request->validate([
    'invoice_id' => ['required', new IntactSeal(Invoice::class, seal: 'financial')],                        // the key
    'invoice_number' => ['required', new IntactSeal(Invoice::class, seal: 'financial', column: 'number')],  // a unique column
]);

$report = Invoice::query()->withSeals()->get()->verifySeals();   // reuses the eager-loaded seals
Invoice::query()->whereSealed()->count();
Invoice::query()->whereNotSealed('identity')->get();
```

new IntactSeal(string $model, ?string $seal = null, ?string $column = null): the input is the model’s key, or the value of column — an invalid column name throws SealingMisconfiguredException::invalidColumn when the rule is built, not when it runs. The model is loaded with verify-on-retrieve suspended, then verified (one seal, or every seal when none is named): a missing model fails like exists, one that is not intact with sentinel::validation.intact\_seal — the status and reason are never revealed.

## Macros

| Macro | Does |
| --- | --- |
| `Http::withIdempotencyKey(?string $key = null)` | Sends the idempotency.header header (Idempotency-Key) with the given key, or a fresh UUIDv7, serialized as an RFC 9651 string. |
| `Http::withSignature(string $keyId, ?SigningOptions $options = null)` | Signs the final request (RFC 9421) with a key of the outbound ring, through SentinelManager::signRequest(). |
| `$collection->verifySeals(?string $seal = null)` | A VerificationReport over an Eloquent collection; reuses eager-loaded seal rows. |

The HTTP-client macros are registered on PendingRequest at boot, and verifySeals() on Eloquent collections — each only when no macro of that name exists yet.

## Query scopes

```php
Invoice::query()->whereSealed()->get();               // has a seal row for the default seal
Invoice::query()->whereSealed('identity')->get();
Invoice::query()->whereNotSealed()->count();          // no seal row (unsealed or deleted)
Invoice::query()->withSeals()->get();                 // eager-load sentinelSeals
```

$invoice->sentinelSeals is the MorphMany relation to the stored Models\\Seal rows.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sentinel-for-laravel.md)
