Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cache For Laravel Sanctum Laravel Package

abe/cache-for-laravel-sanctum

A lightweight Laravel package that adds caching to Sanctum token lookups, reducing database queries on authenticated requests. Speeds up API auth for high-traffic apps while keeping Sanctum's token behavior intact. Easy to install and configure.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is (and what it isn't). abe/cache-for-laravel-sanctum is a small shim around Laravel Sanctum's personal access token model. Sanctum resolves a bearer token by hitting the personal_access_tokens table on every authenticated request (PersonalAccessToken::findToken() → find($id) + hash_equals). This package swaps in a model that serves that lookup from the cache store first, falling back to the database on a miss. It is not a replacement for Sanctum, does not change token issuance, and does not touch guards or middleware.

Before you install, know the state of the project. 17 stars, zero dependents, archived, last tagged release 2023-12-24. Treat it as reference implementation, not infrastructure. The install steps below work, but read the source (src/) before shipping — it is short enough to audit in ten minutes, and short enough to vendor into your own app if you'd rather not take the dependency.

Minimal steps

composer require abe/cache-for-laravel-sanctum
php artisan vendor:publish --tag=sanctum-cache-config   # tag name: check the provider

Register the token model in a service provider so Sanctum resolves through the cached subclass:

// app/Providers/AppServiceProvider.php  (boot)
use Laravel\Sanctum\Sanctum;

public function boot(): void
{
    Sanctum::usePersonalAccessTokenModel(
        \Abe\SanctumCache\Models\CachedPersonalAccessToken::class // confirm FQCN in src/
    );
}

Then point it at a real cache store:

// config/sanctum-cache.php (or wherever the package config lands)
return [
    'store' => env('SANCTUM_CACHE_STORE', 'redis'),
    'ttl'   => env('SANCTUM_CACHE_TTL', 300), // seconds
    'prefix' => 'sanctum:token:',
];

Where to look first

  1. The published config — that's the entire surface area you can tune.
  2. The model class — specifically the overridden findToken(); that's where the cache read/write/forget logic lives.
  3. Any model event hooks in booted() — this is where invalidation happens, and it's the part most likely to be incomplete.

First use case. A high-traffic API where every request carries a Sanctum bearer token and you're seeing select * from personal_access_tokens where id = ? dominate slow-query logs. Put this in front, use Redis, set a TTL of a few minutes, and measure. If your traffic is low or your DB is not the bottleneck, this package adds a moving part for no gain.

Sanity check it worked

DB::listen(fn ($q) => logger($q->sql));
// hit an authenticated endpoint twice
// second request should show zero personal_access_tokens selects

Implementation Patterns

Cache-aside on the token lookup

The core pattern the package implements, useful to understand even if you roll your own:

public static function findToken($token)
{
    [$id, $plain] = array_pad(explode('|', $token, 2), 2, null);

    if ($id === null) {
        return static::where('token', hash('sha256', $token))->first();
    }

    $cached = Cache::store(config('sanctum-cache.store'))
        ->get(config('sanctum-cache.prefix').$id);

    if ($cached === null) {
        $cached = static::find($id);                 // DB hit
        Cache::store(...)->put($key, $cached, $ttl);
    }

    // NEVER trust cached hash blindly — constant-time compare still required
    return $cached && hash_equals($cached->token, hash('sha256', $plain))
        ? $cached
        : null;
}

Two things matter: the cache key is the token id (the prefix before the |), and the hash_equals check stays on the hot path. Caching a lookup is not the same as caching authentication.

Invalidate on every state transition

The only way this is safe is if every write to the model clears the key. Hook the model itself, not the controllers:

protected static function booted(): void
{
    static::saved(fn ($m) => static::forgetCached($m->getKey()));
    static::deleted(fn ($m) => static::forgetCached($m->getKey()));
}

deleted covers $user->tokens()->delete() on logout and currentAccessToken()->delete(). saved covers last_used_at (see Gotchas) and any admin-driven revocation. If the package version you pin only overrides findToken and misses these events, add them in a subclass — this is the single most important thing to verify.

Warm the cache on issuance

The first request after createToken() is always a miss. You can pre-seed it if you're issuing tokens in a burst (CI, mobile login fan-out):

$new = $user->createToken('mobile');
Cache::store('redis')->put(
    'sanctum:token:'.$new->accessToken->getKey(),
    $new->accessToken,
    config('sanctum-cache.ttl')
);

Only worth it if issuance volume is comparable to request volume. For most apps it's noise.

TTL aligned to sanctum.expiration

If you set 'expiration' => 60 in config/sanctum.php, a cache TTL of 300 means a token can stay valid for up to 4 extra minutes after it should have expired. Set cache TTL ≤ sanctum.expiration, or enforce expiry in the comparison step:

if ($cached && $cached->expires_at && $cached->expires_at->isPast()) {
    return null;
}

This is the one place where a short TTL (30–60s) buys you correctness rather than just freshness — at that point Redis round-trips dominate and you're trading little.

Testing

Tests that exercise Sanctum authentication will now consult the cache, and a stale entry from a previous test poisons the next one. Flush per test:

protected function setUp(): void
{
    parent::setUp();
    Cache::store('redis')->flush();   // or use the array store in phpunit.xml
}

Prefer 'default' => 'array' in phpunit.xml for the whole suite — the array store is per-process and dies with the test. If you have tests that specifically assert revocation, add one that revokes and immediately re-authenticates with the old token, expecting 401. That test is your canary for broken invalidation.

Migration path if you don't want the dependency

The whole feature is ~40 lines. Copy the model into app/Models/CachedPersonalAccessToken.php, extend Laravel\Sanctum\PersonalAccessToken, override findToken as above, add the saved/deleted hooks, and register it with Sanctum::usePersonalAccessTokenModel(). You own the invalidation semantics, which for a security-adjacent component is usually the right call for an archived package.

Gotchas and Tips

last_used_at writes are the load you can't cache away. Sanctum's guard force-fills and saves last_used_at on every authenticated request. Caching reads removes the SELECT; it does nothing for the UPDATE. If your goal is DB load reduction, measure the write side too, and consider whether you actually need per-request granularity — many teams drop last_used_at to once-a-minute precision with a conditional update, which is a bigger win than this package.

A cached saved event can resurrect a revoked token. If invalidation is wired to deleted only, then $token->update(['abilities' => []]) or an admin flipping a token's expiry leaves the stale copy in cache until TTL. Always invalidate on saved, not just deleted. Write the revocation test.

Don't cache the plaintext token. The token column is already a SHA-256 hash — that's what Sanctum compares against. If you're storing anything else in the cache payload (raw bearer strings for logging, etc.), stop.

Cache store choice matters more than usual here.

  • file / database stores require serialize/unserialize of an Eloquent model per request — often slower than the indexed primary-key lookup you were trying to avoid.
  • array is per-process; under Octane it persists across requests within a worker, which is either a feature (free hot cache) or a bug (stale after revocation on another worker) depending on your invalidation.
  • Redis is the only store where this reliably pays off, and it's the only one where invalidation propagates across your fleet.
  • cache tags are unsupported on file and database stores — if the package uses tags internally, those stores silently no-op or throw. Check before you configure file.

Multiple app servers = multiple caches. With file or array caches, revoking a token on server A leaves server B authenticating it. Fine for a single-box deploy, unacceptable behind a load balancer. This is a correctness bug, not a performance tradeoff.

Serialized model staleness. You're caching a full Eloquent model. Relations aren't loaded, so $token->tokenable re-queries on first access (fine, expected). But if you've configured Model::preventLazyLoading(), cached models can trip strict-mode assertions that freshly-hydrated models wouldn't. Prefer caching a plain array of the columns you need (id, token, abilities, expires_at) and reconstructing — it's smaller, safer, and version-stable.

Debugging misses. Add a temporary counter or log line inside the cache branch — hit rate is the only metric that tells you whether the package is doing anything. If your hit rate is below ~80%, either your TTL is too short for your key cardinality (lots of distinct tokens, each rarely reused) or your cache is evicting under memory pressure.

Deploy hooks. Any deploy that changes token semantics should flush: php artisan cache:clear (or a namespaced flush on the prefix). Forgetting this after a Sanctum upgrade is a classic source of "works on staging, 401s in prod for 5 minutes."

Extension point to watch. The config's store/ttl/prefix is the intended knob. If you need per-token-type TTLs or a custom invalidation strategy, subclass the model rather than forking — Sanctum::usePersonalAccessTokenModel() accepts any subclass, and that keeps your diff against the package at one constructor.

Archived means no upstream fixes. Pin the exact version in composer.json, and put a // TODO: inline this note in the service provider. If Sanctum changes findToken's signature (it's a public static on a model you don't control), your override breaks silently at upgrade time — run the revocation test in CI after every composer update.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console