- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Cookies vs Tokens: Where Should Your Web App Keep the Session?
About Post
Somewhere right now, a tutorial is telling a new developer to save the login token in localStorage. Three lines of code, works first time, ships to production. And it's the one storage place that any script on the page can read.
"Cookies vs tokens" is one of the most argued-about topics in web development, partly because the question itself is slightly wrong. Let's fix the question first, then answer it for the cases you'll actually meet: a classic web app, a single-page app, and a mobile app.
The question is really "where" and "how"
A cookie is not the opposite of a token. A cookie is a storage and transport mechanism: the browser stores it and automatically attaches it to requests for that site. A token is just a credential, a string that proves who you are. You can put a token inside a cookie.
So the real choice is between two models:
- Browser-managed: the credential lives in an
HttpOnlycookie. Your JavaScript never touches it; the browser sends it automatically. - App-managed: your code stores the token somewhere (often
localStorage) and adds it to each request as anAuthorization: Bearer ...header.
Each model is vulnerable to a different attack, and that's the heart of the whole debate.
The localStorage problem: XSS
Anything in localStorage can be read by any JavaScript running on your page. That includes your code, but also a compromised npm package, a third-party widget, or an attacker who found one unescaped input field (a cross-site scripting bug, XSS).
With one line, that script can read the token and send it to the attacker's server. Now they can use it from their own machine, for as long as the token stays valid. You might not notice for a long time.
An HttpOnly cookie can't be read by JavaScript at all. To be clear, XSS is still very bad with cookies: the attacker's script can make requests as the user while the page is open. But they can't walk away with the credential. That's a meaningful reduction in damage.
The cookie problem: CSRF
The thing that makes cookies convenient also makes them risky. Because the browser attaches them automatically, another website could try to trigger a request to your site, and the user's cookie would ride along. That's cross-site request forgery (CSRF).
The good news is that CSRF is a well-understood, largely solved problem:
SameSitecookies. WithSameSite=Lax(Laravel's default for its session cookie), the browser doesn't send the cookie on cross-site POST requests.Strictgoes further.- CSRF tokens. The server requires a secret value that a foreign site can't read. Laravel's
@csrfand itsXSRF-TOKENcookie handle this for you. Secureflag. The cookie is only ever sent over HTTPS.
Bearer tokens in headers don't have the CSRF problem, because the browser never adds them automatically. But they trade it for the XSS exfiltration problem above.
Side by side
| HttpOnly cookie | Token in localStorage | |
|---|---|---|
| Readable by JavaScript | No | Yes |
| If there's an XSS bug | Attacker acts as the user only while the page is open | Attacker can steal the token and reuse it elsewhere |
| CSRF risk | Yes, mitigated with SameSite and CSRF tokens | No |
| Works across different domains | Awkward | Easy |
| Works for native mobile apps | Not the natural fit | Yes, but use secure storage instead |
| Best for | Web apps and SPAs on your own domain | Rarely the best choice in a browser |
My rule: browser gets cookies, apps get tokens
Rule of thumb: if your frontend runs in a browser on the same site as your API, use HttpOnly session cookies. If your client is a mobile app or a third-party service, use tokens, stored in the platform's secure storage.
Mobile apps don't share the browser's cookie model, and they don't have the same XSS exposure. A token stored in the iOS Keychain or Android Keystore (in Expo, via expo-secure-store) and sent in the Authorization header is the standard approach.
How Laravel Sanctum does both
Laravel Sanctum is a nice example of this rule in practice, because it has two modes in one package.
Mode 1: SPA authentication (cookies)
For a React or Vue frontend on the same top-level domain as your API, Sanctum uses Laravel's normal session cookies. The SPA first fetches a CSRF cookie, then logs in:
axios.defaults.withCredentials = true;
axios.defaults.withXSRFToken = true;
await axios.get('/sanctum/csrf-cookie');
await axios.post('/login', { email, password });
// From now on, the session cookie is sent automatically
const { data } = await axios.get('/api/user');
On the Laravel 11 side, you enable this with $middleware->statefulApi() in bootstrap/app.php and list your frontend domain in SANCTUM_STATEFUL_DOMAINS. No token ever touches JavaScript.
Mode 2: API tokens (mobile apps and integrations)
For a mobile app, Sanctum issues tokens. A simplified login endpoint, close to the one in the docs:
Route::post('/mobile/token', function (Request $request) {
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
return ['token' => $user->createToken($request->device_name)->plainTextToken];
});
Sanctum stores only a hash of the token in the database, so a database leak doesn't expose usable tokens. Because tokens are stored server-side, you can revoke one device ("log out of my old phone") by deleting its row. You'd also want rate limiting on this route in production.
A quick word on JWTs
JWTs are often what people mean by "tokens". They're self-contained and signed, so the server can verify them without a database lookup. The flip side: once issued, a JWT is valid until it expires, and revoking one early needs extra machinery like a deny list. For most first-party apps, a server-side session or a database-backed token like Sanctum's is simpler and easier to revoke. JWTs shine more when separate services need to verify identity independently.
The wrap-up
- Cookies vs tokens is really browser-managed vs app-managed credentials.
localStorageturns any XSS into a stolen credential. Avoid it for auth tokens.- HttpOnly + Secure + SameSite cookies, plus CSRF protection, is the strong default for browser apps.
- Mobile apps use tokens in secure storage, sent as Bearer headers.
- Sanctum gives you both, so you don't have to choose one for everything.
The Sanctum documentation covers both modes in detail, including the domain configuration that trips most people up.
Where do your apps keep the session today, and was that a deliberate choice or something a tutorial decided for you?

Be first to comment it...