shetabit/token-builder
Shetabit Token Builder is a Laravel package for generating unique, customizable tokens and attaching them to Eloquent models. Configure character sets, length, prefixes, and uniqueness rules to create API keys, activation codes, or reference strings with simple, fluent methods.
This package issues unique, opaque tokens for any Eloquent model (API keys, invite codes, password-reset links, short URLs) and later resolves a token string back to its owning model.
Install and publish
composer require shetabit/token-builder
php artisan vendor:publish --tag="token-builder-config"
php artisan vendor:publish --tag="token-builder-migrations"
php artisan migrate
Where to look first
config/token-builder.php — token length, allowed characters, prefix, expiry/lifetime, and the bound Token model class. This is the file you will touch most.token column is unique and indexed, and that a nullable expires_at exists.Token model — it holds the polymorphic tokenable relation and the expiry/validity helpers.First use case — issue a token
use Shetabit\TokenBuilder\Facade\TokenBuilder;
$token = TokenBuilder::model($user)->create();
// persist/send $token->token to the client
Then resolve it on the way back in:
$token = TokenBuilder::token($request->bearerToken())->find(); // null when invalid/expired
If the package registers a tokens() morph relation on the tokenable model, $user->tokens gives you the inverse for free.
Issue-on-login / issue-on-registration. Generate the token inside the auth flow and return it once in the response body. Treat it like a password: it is a bearer credential, so never log it and never put it in a URL you don't control.
Resolve in middleware. Wrap TokenBuilder::token(...)->find() in a small middleware or a guard, resolve the owning model, and Auth::setUser($model) — this lets you reuse Laravel's authorization layer instead of hand-rolling checks.
The v4 headline feature: enumerate a model's valid tokens without knowing the token string. Previously tokens were write-only from the app's perspective — you could only look one up if the client presented it. v4 lets you query a model's valid (non-expired, non-revoked) tokens directly, which unlocks:
Guard against token sprawl. Decide the policy up front: one token per model, or many (one per device). If it's one, revoke the previous token inside the same transaction as the new one is created.
Prune on a schedule. Expired rows accumulate in the tokens table. Add a scheduled command (or model prune-ing) that deletes rows past expires_at; the v4 listing API makes it cheap to check what's still live.
Custom Token model. If you need extra columns (name, last_used_at, user agent, IP), extend the package model, point the config at your class, and add the columns via your own migration. This is the intended extension point and keeps you off the package's internals.
Testing. Use Carbon::setTestNow() to exercise expiry boundaries rather than sleeping, and assertDatabaseHas on the token table to verify issuance. For HTTP tests, issue a token in setUp and pass it as a bearer header.
v3.0.0...v4.0.0 diff before upgrading, re-publish the config and migration, and run your token tests. Do not assume a drop-in upgrade.tokens table has working credentials. If that's unacceptable, hash on write and hash the incoming token before lookup — but note that hashing breaks the plaintext-based listing feature, so plan the trade-off deliberately.where token = ?. Confirm the unique index actually exists in the published migration; a table scan here is a silent performance cliff.varbinary/ascii_bin, or at minimum know which behaviour you have.expires_at null semantics. Null almost always means "never expires" — verify in the package's validity check, and be explicit in your own code so a config change can't silently make tokens immortal.expires_at comparisons use the app timezone against a DB-stored timestamp. Keep config('app.timezone') and the DB session timezone aligned, or short-lived tokens will expire early/late.where expires_at null or > now) — don't assume the name matches the behaviour.onDelete('cascade') on the morph relation, deleting a user orphans their tokens. Check it, and if it's missing, clean up in a model observer.Bearer prefix stripped?), the row exists, the row isn't expired, the tokenable model still exists. Nine times out of ten it's a stale row from a soft-deleted parent.How can I help you explore Laravel packages today?