Date: 2026-01-31
Context: Fastify + Better Auth integration - API endpoints returning 401 even with valid session tokens
After implementing Better Auth with Fastify following the official integration guide, API endpoints were returning 401 Unauthorized even with valid session tokens from the production application.
The web app worked fine. The database had the session record. But curl requests with Authorization: Bearer <token> kept failing. No obvious errors in server logs.
Attempt 1: Checked if the session existed in the database → Session was there. Token ID matched. Still 401.
Attempt 2: Tried using the full token (with signature) in database queries
→ No results. Realized the database only stores the token ID (before the .), not the signature.
Attempt 3: Used Authorization: Bearer <token> header in curl
→ Still 401. Better Auth wasn't recognizing it.
Attempt 4: Switched to Cookie: better-auth.session_token=<token>
→ WORKED on localhost! But failed when using production tokens.
Attempt 5: Realized production uses __Secure-better-auth.session_token
→ Tried that on localhost → Failed. The __Secure- prefix requires HTTPS.
Attempt 6: Used unprefixed cookie name for localhost, prefixed for production → WORKED everywhere!
Better Auth uses cookie-based sessions, not Bearer tokens.
For localhost (HTTP):
curl -H "Cookie: better-auth.session_token=<token>" http://localhost:3001/api/v1/tripsFor production (HTTPS):
curl -H "Cookie: __Secure-better-auth.session_token=<token>" https://api.example.com/v1/tripsToken structure:
- Cookie value:
{tokenId}.{signature}(e.g.,rBhozMGKtQ5ujZ40W94AihzEOZpG4UYH.f3wILxj3sXOeD23DTSmrwssFAXupB1R6IbaguhgoY1w=) - Database stores: Only
{tokenId}(e.g.,rBhozMGKtQ5ujZ40W94AihzEOZpG4UYH) - Validation: Better Auth looks up the token ID, then verifies the signature
Better Auth is session-based (cookie authentication), NOT a JWT/Bearer token system.
The __Secure- prefix is a browser security standard that requires HTTPS. Browsers won't send __Secure- cookies over HTTP connections. That's why localhost needs the unprefixed version.
- Better Auth ≠ Bearer tokens. It's cookie-based. Using
Authorization: Bearerdoesn't work. __Secure-prefix requires HTTPS. Use unprefixed cookie names for local HTTP development.- Database only stores token ID. The signature is validated at runtime, not stored.
- Fastify headers need conversion. Better Auth expects standard
Headersobjects (Fetch API), not Fastify's plain JavaScript object.
apps/server/src/plugins/auth.ts- Auth plugin withreq.getSession()decoratorapps/server/src/routes/v1/trips.ts- Example protected route
When testing authenticated endpoints:
- Always use cookie-based auth, not Bearer tokens
- Use
better-auth.session_tokenfor localhost - Use
__Secure-better-auth.session_tokenfor production - Test the
/api/auth/get-sessionendpoint first to verify auth is working - Check the database for the token ID (before the
.), not the full token