How to Add Authentication to a Nuxt App
Add auth to your Nuxt app with route middleware, sealed cookie sessions, and server routes. Step-by-step code examples plus a faster shortcut.
You want to add auth to your Nuxt app, and the first thing you notice is that Nuxt doesn’t ship with a login system. There’s no useAuth() waiting for you, no built-in /login page. You get the building blocks — route middleware, server API routes, useState, useCookie — and you’re expected to assemble them yourself.
That’s fine once you know how the pieces fit. The confusing part is that Nuxt runs your code in two places: on the server during the initial request, and in the browser after hydration. Auth has to work in both, or you get logged-in users seeing a flash of the login screen, or protected pages briefly rendering before a redirect kicks in.
This guide walks through adding authentication to a Nuxt 3/4 app two ways: rolling your own session handling with the official patterns, and using a module that does the heavy lifting. Both are valid. Pick the one that matches how much time you have.
Why Auth Is Mostly Boilerplate
Here’s the thing nobody tells you when you start: the interesting part of your app is not the login form. It’s whatever you’re building behind the login form. But auth eats a surprising amount of time because it’s not just “check a password.”
A real auth setup needs password hashing, session creation, a cookie that survives page reloads, server-side validation so people can’t fake their way in, route protection on both the client and server, a logout flow, and ideally password reset and email verification. Then if you want to charge money, you bolt Stripe on top — checkout, webhooks, subscription state, and tying all of that back to the user record.
None of it is hard in isolation. It’s the volume and the edge cases that get you. This is the same boilerplate you’d write for Next.js or Astro — only the framework syntax changes.
Your Nuxt Auth Options
You’ve got three realistic paths in Nuxt:
1. Roll your own with server routes + useCookie. Maximum control, zero dependencies beyond what’s in Nuxt. Good for learning and for apps with unusual requirements. You write everything.
2. nuxt-auth-utils. A small module from the Nuxt core team (atinux) that handles sealed cookie sessions for you. No database needed for session storage — session data lives in an encrypted cookie. This is the sweet spot for most indie projects that want to self-host auth.
3. @sidebase/nuxt-auth. A heavier module that wraps Auth.js (NextAuth’s framework-agnostic sibling) plus a credentials/local provider. Good if you want OAuth providers (Google, GitHub) with minimal config.
For this guide I’ll show the nuxt-auth-utils approach for the DIY half, because it’s the closest thing to an “official” recommendation and it’s genuinely small. If you’re still picking between hosted auth providers, we compared a few in Auth0 vs Clerk vs Supabase Auth.
Setting Up Sessions with nuxt-auth-utils
Install it and add it to your modules:
npx nuxi module add auth-utils
That registers nuxt-auth-utils in nuxt.config.ts. You also need a session secret — it’s used to encrypt (seal) the cookie so nobody can tamper with the session client-side:
# .env
NUXT_SESSION_PASSWORD=a-random-string-at-least-32-characters-long
Now create a login server route. Server routes live under server/api/ and run only on the server, so this is the safe place to check passwords:
// server/api/login.post.ts
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
// Look up the user in your DB and verify the hash
const user = await verifyCredentials(email, password)
if (!user) {
throw createError({ statusCode: 401, message: 'Invalid credentials' })
}
// Seal the user into an encrypted cookie session
await setUserSession(event, {
user: {
id: user.id,
email: user.email,
},
})
return { ok: true }
})
setUserSession is provided by the module. It serializes the data, encrypts it with your NUXT_SESSION_PASSWORD, and sets an httpOnly cookie. No session table required.
On the client, the useUserSession composable tells you who’s logged in. It works during SSR and after hydration, which is the part that’s annoying to get right by hand:
<!-- app.vue or a header component -->
<script setup>
const { loggedIn, user, clear } = useUserSession()
async function login() {
await $fetch('/api/login', {
method: 'POST',
body: { email: email.value, password: password.value },
})
// Refresh the session state from the sealed cookie
await useUserSession().fetch()
await navigateTo('/dashboard')
}
async function logout() {
await clear() // clears the session cookie
await navigateTo('/login')
}
</script>
<template>
<div v-if="loggedIn">
Signed in as {{ user.email }}
<button @click="logout">Log out</button>
</div>
</template>
Adding Protected Routes with Middleware
This is where Nuxt’s two-environment model matters. There are two layers to protect, and you want both.
Client-side: route middleware. Middleware files go in middleware/. A named middleware runs only on pages that opt in via definePageMeta. Create one for auth:
// middleware/auth.ts
export default defineRouteMiddleware(() => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) {
return navigateTo('/login')
}
})
Then on any page you want to gate:
<!-- pages/dashboard.vue -->
<script setup>
definePageMeta({
middleware: ['auth'],
})
const { user } = useUserSession()
</script>
<template>
<h1>Welcome back, {{ user.email }}</h1>
</template>
A quick note on global middleware: you can add a .global suffix to run middleware on every route, but if you do that naively you’ll lock yourself out of the login page itself. If you go global, exempt your public routes explicitly — or just use named middleware like above and apply it only where you need it.
Server-side: validate the session in your API routes. Client middleware is a UX nicety — it stops the page from rendering. It does not protect your data, because anyone can hit your API directly. For that, use requireUserSession in protected server routes. It throws a 401 if there’s no valid session:
// server/api/me.get.ts
export default defineEventHandler(async (event) => {
const { user } = await requireUserSession(event)
// user is guaranteed to exist here
const data = await getUserData(user.id)
return data
})
Rule of thumb: client middleware for redirects, requireUserSession for anything that touches real data. If you only do one, do the server-side one.
How Sessions Actually Work Here
Worth understanding so you can debug it later. With nuxt-auth-utils, there’s no server-side session store. The session is the cookie — the user object is encrypted with your NUXT_SESSION_PASSWORD and written to an httpOnly, sealed cookie. On each request, Nuxt decrypts it and hands you the data.
The upside: no database table, no Redis, dead simple to deploy. The tradeoff: you can’t instantly revoke a single session server-side (the cookie is valid until it expires), and you shouldn’t stuff large objects in there because cookies have a ~4KB limit. Keep the session to an ID and a few fields, and fetch the rest from your DB.
If you’d rather do raw useCookie + useState without a module, the pattern is the same idea: store a token in a cookie via useCookie('token'), sync it into reactive state with useState, and validate it in a global middleware that calls /api/me. The module just saves you from writing the encryption and the SSR sync code.
Adding Stripe Payments After Auth
Once you know who the user is, payments are the logical next step — and they’re the other half of the boilerplate. The direct route looks like this:
- Create a Stripe Checkout session in a server route, passing the logged-in user’s ID as metadata.
- Redirect the user to Stripe’s hosted checkout.
- Handle the
checkout.session.completedwebhook in another server route to mark the user as paid. - Store subscription status against the user and check it before serving paid features.
// server/api/checkout.post.ts
import Stripe from 'stripe'
export default defineEventHandler(async (event) => {
const { user } = await requireUserSession(event)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: 'price_xxx', quantity: 1 }],
success_url: 'https://yourapp.com/dashboard',
cancel_url: 'https://yourapp.com/pricing',
metadata: { userId: user.id },
})
return { url: session.url }
})
That’s the happy path. The parts that take real time are webhook signature verification, handling failed payments and cancellations, syncing subscription state, and not double-charging people. It’s a project on its own.
If you’d rather not build auth and a Stripe integration from scratch, that’s exactly the gap Beag fills. You add auth and payments to your app in about 5 minutes — login, signup, password reset, and Stripe billing all wired together — instead of spending a weekend on session encryption and webhook handlers. Your Nuxt app stays focused on the actual product. We broke down where this fits in a typical indie hacker tech stack too.
Which Approach Should You Use?
Build it yourself with nuxt-auth-utils if: you want to keep auth in-house, you enjoy owning the flow, and you’re fine writing password reset and email verification later. It’s a clean, well-supported path.
Use Beag if: you want auth and Stripe payments working today, you’re validating an idea and don’t want to maintain webhook handlers, or you’d rather spend your time on the product than on the plumbing.
Either way, the technical concepts above carry over — sessions, middleware, server-side validation. Knowing how they work makes you better at debugging no matter which route you take.
FAQ
Does Nuxt have built-in authentication?
No. Nuxt gives you the primitives — server API routes, route middleware, useState, and useCookie — but no ready-made login system. You either assemble auth from those pieces, add a module like nuxt-auth-utils or @sidebase/nuxt-auth, or use a hosted service.
What’s the difference between nuxt-auth-utils and @sidebase/nuxt-auth?
nuxt-auth-utils is lightweight and handles sealed cookie sessions with no external dependency — great for credentials-based login you control. @sidebase/nuxt-auth wraps Auth.js and is the better fit when you want OAuth providers (Google, GitHub, etc.) with minimal setup. Start with auth-utils if you just need email/password sessions.
Do I need a database to store sessions in Nuxt?
Not with nuxt-auth-utils. It stores the session inside an encrypted, sealed cookie, so there’s no session table or Redis required. You’ll still want a database for your user records, but the session itself rides in the cookie. The tradeoff is you can’t instantly revoke individual sessions server-side.
How do I protect a single page in Nuxt?
Add a named middleware (e.g. middleware/auth.ts) that checks loggedIn and redirects to /login, then opt that page in with definePageMeta({ middleware: ['auth'] }). For data protection, also validate the session in your server routes with requireUserSession. Client middleware handles the redirect; server validation handles security.
Can I add Stripe payments to a Nuxt app?
Yes. Create Stripe Checkout sessions in a server/api/ route, redirect to Stripe’s hosted checkout, and handle the checkout.session.completed webhook in another server route to update subscription state. You can build this directly with Stripe’s Node SDK, or use Beag to get auth and Stripe billing pre-wired.
Ready to skip the auth and billing plumbing? Add auth + payments to your Nuxt app in 5 minutes. Or read the docs to see how it fits your stack.
For more framework guides, check out the Beag blog.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →