I've set up Sanctum in probably 8 or 9 Laravel projects at this point — payroll systems, SaaS tools, e-commerce APIs. Every time I onboard a junior dev or review someone's code, I see the same 3-4 mistakes repeated. So here's the setup that works, plus the exact things that will silently break it.
First: what Sanctum actually does
Sanctum issues a plain-text token, stores a hashed version in your personal_access_tokens table, and validates it on every request with a database lookup. That's it. No JWT, no signatures, no decoding — just a DB row.
This is why token revocation is trivially easy with Sanctum and nightmarish with JWT. Delete the row, the token is dead instantly.
The setup (Laravel 12)
Step 1 — Install (Laravel 10 and below only)
Laravel 11 and 12 ship with Sanctum already. If you're on 10:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Step 2 — Add the trait to your User model
This is the one people forget most often:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
}
Without this, createToken() doesn't exist on your model, and you'll get a cryptic error.
Step 3 — Routes
Route::prefix('v1')->group(function () {
// Public
Route::post('auth/register', [AuthController::class, 'register']);
Route::post('auth/login', [AuthController::class, 'login']);
// Protected
Route::middleware('auth:sanctum')->group(function () {
Route::post('auth/logout', [AuthController::class, 'logout']);
Route::get('auth/me', [AuthController::class, 'me']);
});
});
Step 4 — The AuthController
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = Auth::user()->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => Auth::user(),
'token' => $token,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out']);
}
Step 5 — Send the token
Authorization: Bearer 1|yourtokenhere...
Note the format — 1|randomstring. The number is the token ID in the database. Send the full string.
The 4 things that will silently break your setup
1. Forgetting HasApiTokens on the User model
You'll get a "method not found" error on createToken(). Always check this first.
2. Sending the token wrong
It must be Authorization: Bearer YOUR_TOKEN. Not a query param, not a cookie, not Token YOUR_TOKEN. Bearer, exactly.
3. Not running migrations
The personal_access_tokens table doesn't exist yet. Run php artisan migrate and confirm the table is there.
4. CORS blocking your frontend
Add your frontend URL to config/cors.php:
'allowed_origins' => ['https://yourfrontend.com'],
'supports_credentials' => true,
Revoking tokens
This is where Sanctum shines vs JWT:
// Logout current device
$request->user()->currentAccessToken()->delete();
// Logout all devices
$user->tokens()->delete();
With JWT, you'd need a denylist and extra infrastructure. With Sanctum, it's one line.
Setting token expiry
In config/sanctum.php:
// Expire after 60 days (value is in minutes)
'expiration' => 60 * 24 * 60,
// Never expire
'expiration' => null,
Should you use Sanctum or JWT?
Use Sanctum if you're building an API for your own app (SPA, mobile). Use JWT only if you need stateless cross-service auth in a microservices setup. For 90% of Laravel projects, Sanctum is the right call.
I wrote a full breakdown here: Does Laravel Sanctum use JWT? →
Full tutorial
If you want the complete version with register, me endpoint, token abilities, and more — I wrote it up here:
👉 Laravel Sanctum Setup Tutorial (2026)
Already using Sanctum?
I built a Laravel API Starter Kit with Sanctum pre-configured — auth, roles, pagination, API versioning all included. Saves a few hours on every new project. $19 on Gumroad if you want to check it out.
What issues have you run into with Sanctum? Drop them below — happy to help debug.
Top comments (0)