NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Sentinel for Laravel

The Sentinel facade

Everything goes through one facade, RoundlyConsulting\Sentinel\Facades\Sentinel — import it, there is no global alias. for($model) returns a handle bound to one seal, flat verbs act on the model you hand them, model($class) works on every row of a model, and five sub-accessors group the rest: keys(), ledger(), idempotency(), nonces() and signatures().

use RoundlyConsulting\Sentinel\Enums\Algorithm;
use RoundlyConsulting\Sentinel\Facades\Sentinel;

// One model: a handle bound to a seal (the default one when unnamed)
Sentinel::for($invoice)->verify();
Sentinel::for($invoice, 'identity')->verify();
Sentinel::for($invoice)->by($admin)->because('INC-88: refund fixed by the DBA')->acknowledge();

// Flat verbs act on the model you hand them
Sentinel::seal($invoice, reason: 'Recomputed lines');
Sentinel::verify($invoice, 'financial');
Sentinel::verifyAll($invoice)->allIntact();

// Every row of a model
Sentinel::model(Invoice::class)->scan();

// Sub-accessors
Sentinel::keys()->ring('http')->import('acme-2026-10', Algorithm::Ed25519, $partnerPublicKeyPem, owner: $partner);
Sentinel::ledger()->checkpoint();
Sentinel::idempotency()->run("charge:{$order->id}", scope: 'billing', callback: fn () => $gateway->charge($order));
Sentinel::nonces()->issue('password-reset', ttl: 900, subject: $user);
Sentinel::signatures()->current($request);

Model seals

A seal name the model does not declare throws SealingMisconfiguredException::unknownSeal (the message lists the declared seals); a null seal is the model’s default.

MethodReturnsDoes
for(Model $model, ?string $seal = null)SealHandleA handle bound to one seal (the default one when unnamed).
model(string $class)ModelSealsClass-level operations over every row of a model.
sealables()list<class-string<Model>>sentinel.models first, then every class with seal rows or ledger entries.
check()HealthReportThe installation health check.
seal(Model $model, ?string $seal = null, ?string $reason = null, ?Model $actor = null)SealResultExplicit seal; refused on a tampered model.
verify(Model $model, ?string $seal = null)VerificationResultVerify one seal.
verifyOrFail(Model $model, ?string $seal = null)VerificationResultVerify or throw TamperedModelException.
verifyAll(Model $model)VerificationReportEvery declared seal of one model.
verifyMany(iterable $models, ?string $seal = null)VerificationReportMany models; a null seal = every seal of each.
isIntact(Model $model)boolTrue when every seal is intact.
acknowledge(Model $model, string $reason, ?Model $actor = null, ?string $seal = null)AcknowledgementResultAccept an out-of-band change with a reason.
unseal(Model $model, string $reason, ?Model $actor = null, ?string $seal = null)boolRemove a seal deliberately; returns whether a seal row existed.
ledgerHistory(Model $model, ?string $seal = null, int $limit = 50)list<LedgerRecord>Ledger records, newest first (limit 1–1000).
currentSeal(Model $model, ?string $seal = null)?SealRecordThe stored seal row, unverified.
scan(ScanOptions $options)ScanReportChunked verification of every row.
reseal(ResealOptions $options)ResealReportRe-seal intact rows with the current key or definition.
resealWhere(ResealWhereRequest $request)ResealReportAcknowledge every selected row.
updateAndReseal(UpdateAndResealRequest $request)ResealReportA verified mass update.
sealMissing(BaselineOptions $options)ResealReportBaseline rows that never had a seal.
withoutSealing(Closure $callback, string $reason)mixedRun the callback with sealing suspended.
withoutVerification(Closure $callback)mixedRun the callback with verify-on-retrieve suspended.
$result  = Sentinel::seal($invoice, reason: 'Recomputed lines');           // SealResult
$result  = Sentinel::verify($invoice);                                       // the default seal
$result  = Sentinel::verifyOrFail($invoice, 'financial');
$report  = Sentinel::verifyAll($invoice);                                    // every seal
$report  = Sentinel::verifyMany(Invoice::query()->latest()->limit(50)->get(), 'financial');
$ok      = Sentinel::isIntact($invoice);
$removed = Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity');
$history = Sentinel::ledgerHistory($invoice, 'financial', limit: 20);       // list<LedgerRecord>
$stored  = Sentinel::currentSeal($invoice);                                  // ?SealRecord
$classes = Sentinel::sealables();                                            // [Invoice::class, …]

Keys

MethodReturnsDoes
keys()KeysAccessorRings, key inventory and ring handles.
generateKey(GenerateKeyRequest $request)GeneratedKeyGenerate a key (database by default, or env lines).
importKey(ImportKeyRequest $request)KeyInfoImport a partner’s (or your own) key.
rotateKey(RotateKeyRequest $request)RotationResultRotate a ring’s signing key.
revokeKey(RevokeKeyRequest $request)KeyInfoRevoke a database key (reason required).
retireKey(string $ring, string $keyId)KeyInfoEnd a database key’s verification period now.
listKeys(?string $ring = null)list<KeyInfo>Null = every ring; never material.
findKey(string $ring, string $keyId)?KeyInfoOne key in one ring.
currentKey(?string $ring = null)KeyInfoThe signing key, or NoSigningKeyException.
extend(string $driver, Closure $factory)staticRegister a key-store driver.

Ledger

MethodReturnsDoes
ledger()LedgerAccessorCheckpoints, verification, history, head, anchors.
checkpoint(?CheckpointOptions $options = null)?CheckpointResultOne batch; null = nothing pending.
verifyLedger(?LedgerVerifyOptions $options = null)LedgerReportCheckpoints, anchors, pending entries, entity heads.
ledgerHead(?string $connection = null)?CheckpointRecordThe newest checkpoint, unverified.
anchors()list<string>The configured anchor names.
extendAnchor(string $driver, Closure $factory)staticRegister an anchor driver.

Idempotency, nonces and pruning

MethodReturnsDoes
idempotency()IdempotencyAccessorrun() and forget() for jobs, commands and webhooks.
runIdempotent(IdempotentCall $call)IdempotentResultRun a callback at most once per key and scope.
forgetIdempotencyKey(string $key, string $scope)boolWhether the key existed.
nonces()NoncesAccessorIssue, consume, single-use URLs.
issueNonce(IssueNonceRequest $request)IssuedNonceA purpose-bound, single-use token.
consumeNonce(ConsumeNonceRequest $request)boolTrue exactly once.
signedRoute(SignedRouteRequest $request)stringA single-use signed URL.
prune(?PruneOptions $options = null)PruneResultDelete expired idempotency keys and nonces.

HTTP message signatures

MethodReturnsDoes
signatures()SignaturesAccessorSign, verify, current, owner, contentDigest.
signRequest(RequestInterface $request, string $keyId, ?SigningOptions $options = null)RequestInterfaceSign any PSR-7 request.
verifyRequestSignature(Request $request, ?string $profile = null)VerifiedSignatureOr HttpSignatureException (401).
verifyResponseSignature(ResponseInterface|ClientResponse $response, ?string $profile = null)VerifiedSignatureA signed response (PSR-7 or Laravel client).
verifiedSignature(Request $request)?VerifiedSignatureWhat sentinel.signed verified on this request.
signatureOwner(Request|VerifiedSignature $from)?ModelThe owner of the signing key.

Handles and sub-accessors

All are final readonly objects returned by the manager; each method calls one manager method. A ring handle refuses a key id of another ring (UnknownKeyException), and a ModelSeals query of another model class is refused (SealingMisconfiguredException::queryModelMismatch). ModelSeals methods take the same defaults as the option objects (chunk 500, checkLedger true, maxFindings 1000); checkSchema and upgradeFormat exist only on ScanOptions / ResealOptions and the commands.

EntryClassMethods
Sentinel::for($model, ?$seal)SealHandleby(?Model $actor), because(string $reason), name(), definition(), seal(), verify(), verifyOrFail(), isIntact() (this seal only), acknowledge(?string $reason = null), unseal(?string $reason = null), current(), history(int $limit = 50)
Sentinel::model(Invoice::class)ModelSealsdefinition(?string $seal = null), seals(), scan(…), reseal(…), resealWhere(…), updateAndReseal(…), sealMissing(…), find($id), findOrFail($id), unsealedQuery(?string $seal = null)
Sentinel::keys()Accessors\KeysAccessorring(?string $ring = null), all(), rings()
Sentinel::keys()->ring($ring)Accessors\KeyRingHandlename(), current(), find(string $keyId), all(), generate(…), import(…), rotate(?Algorithm $algorithm = null, ?CarbonInterface $activatesAt = null), revoke(string $keyId, string $reason, ?Model $actor = null), retire(string $keyId)
Sentinel::ledger()Accessors\LedgerAccessorcheckpoint(?string $connection = null), verify(?string $connection = null, bool $entities = true, int $chunk = 1000), history(Model $model, ?string $seal = null, int $limit = 50), head(?string $connection = null), anchors()
Sentinel::idempotency()Accessors\IdempotencyAccessorrun(string $key, string $scope, Closure $callback, ?string $fingerprint = null, ?int $ttl = null, ?int $lease = null), forget(string $key, string $scope)
Sentinel::nonces()Accessors\NoncesAccessorissue(string $purpose, ?int $ttl = null, ?Model $subject = null), consume(…), consumeOrFail(…), signedRoute(string $name, array $parameters = [], ?int $ttl = null)
Sentinel::signatures()Accessors\SignaturesAccessorsign(…), verify(Request $request, ?string $profile = null), verifyResponse(…), current(Request $request), owner(Request|VerifiedSignature $from), contentDigest(string $body, DigestAlgorithm $algorithm = DigestAlgorithm::Sha256)

The model trait shorthand

The HasSeals trait, its query scopes and the verifySeals() collection macro call the same manager — so Sentinel::fake() sees them too:

$invoice->seal('financial');                         // SealResult
$invoice->verifySeal('identity');                    // VerificationResult
$invoice->verifySealOrFail();                        // TamperedModelException when not intact
$invoice->isIntact();                                // every seal
$invoice->acknowledgeTampering('INC-88: refund fixed by the DBA', $admin);

Invoice::query()->whereSealed()->count();
Invoice::query()->whereNotSealed('identity')->get();
Invoice::query()->withSeals()->get()->verifySeals();

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

By donating, you agree to our donation terms.

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.