How to Add Auth to Your Astro App in 5 Minutes

22 Mar 2026 · Bank K.

Add authentication to your Astro app with middleware, protected routes, and session handling. Code examples and step-by-step setup included.

You shipped a landing page with Astro and it’s fast. Static HTML, zero JavaScript, perfect Lighthouse scores. Then someone asks “can I log in?” and suddenly you’re reading about SSR adapters, session cookies, and middleware chains at 2 AM.

Astro was built for content sites, but it handles server-side rendering just fine — and that’s exactly what you need for auth. The trick is knowing which pieces to turn on and how to wire them together without overcomplicating your project.

This guide covers two approaches: building cookie-based auth yourself using Astro’s middleware, and dropping in an all-in-one solution that handles auth and Stripe payments together. Pick whichever matches your timeline.

How Auth Works in Astro

Astro defaults to static site generation. Every page is pre-rendered at build time, which means there’s no server to check session cookies or validate tokens. To add auth, you need to switch to on-demand rendering — what Astro calls output: 'server' mode.

In Astro 5, the old output: 'hybrid' option was removed. You now have two choices:

  • output: 'static' — pages are static by default, but individual pages can opt into SSR with export const prerender = false
  • output: 'server' — pages are server-rendered by default, and individual pages can opt into prerendering with export const prerender = true

For auth, output: 'server' makes more sense. Most of your app needs session checking, and you can prerender the few public pages that don’t.

You also need an adapter. Astro supports Node.js, Vercel, Netlify, and Cloudflare out of the box:

npx astro add node
# or: npx astro add vercel
# or: npx astro add netlify

Your astro.config.mjs should look like this:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});

With that in place, every request hits a server, and you can use middleware to check auth state before the page renders.

Option 1: Build Auth with Astro Middleware

Astro middleware runs on every incoming request before the page renders. It’s the right place to validate sessions, set user data, and redirect unauthenticated visitors.

Set Up the Middleware

Create src/middleware.ts and use defineMiddleware for type safety:

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

const PUBLIC_ROUTES = ['/', '/login', '/signup', '/api/auth/login', '/api/auth/signup'];

export const onRequest = defineMiddleware(async (context, next) => {
  const { pathname } = context.url;

  // Skip auth check for public routes and static assets
  if (PUBLIC_ROUTES.includes(pathname) || pathname.startsWith('/_')) {
    return next();
  }

  const sessionToken = context.cookies.get('session')?.value;

  if (!sessionToken) {
    return context.redirect('/login');
  }

  // Validate the session and attach user to locals
  const user = await validateSession(sessionToken);

  if (!user) {
    context.cookies.delete('session', { path: '/' });
    return context.redirect('/login');
  }

  context.locals.user = user;
  return next();
});

The key concept: context.locals is an object shared between middleware, API routes, and .astro pages within a single request. Set the user in middleware, and it’s available everywhere downstream.

Add the Login API Route

// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';
import bcrypt from 'bcryptjs';
import { v4 as uuid } from 'uuid';
import { db } from '../../lib/db';

export const POST: APIRoute = async ({ request, cookies, redirect }) => {
  const formData = await request.formData();
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  const user = await db.user.findUnique({ where: { email } });
  if (!user) {
    return new Response('Invalid credentials', { status: 401 });
  }

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) {
    return new Response('Invalid credentials', { status: 401 });
  }

  // Create session
  const token = uuid();
  await db.session.create({
    data: {
      token,
      userId: user.id,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  cookies.set('session', token, {
    path: '/',
    httpOnly: true,
    sameSite: 'lax',
    secure: true,
    maxAge: 60 * 60 * 24 * 7, // 7 days
  });

  return redirect('/dashboard');
};

Use the User in Pages

Once middleware sets context.locals.user, you can access it in any .astro page:

---
// src/pages/dashboard.astro
const user = Astro.locals.user;
---

<html>
  <body>
    <h1>Welcome, {user.email}</h1>
    <p>You're logged in.</p>
    <a href="/api/auth/logout">Log out</a>
  </body>
</html>

Type the Locals Object

Astro lets you add types for locals so you get autocomplete and type checking:

// src/env.d.ts
/// <reference types="astro/client" />

declare namespace App {
  interface Locals {
    user?: {
      id: string;
      email: string;
    };
  }
}

This approach works. But you’re now responsible for password reset flows, email verification, session cleanup, rate limiting, and CSRF protection. For a side project, that’s a lot of surface area to maintain.

Option 2: Add Auth + Payments with Beag

If you’re building a product — not a learning exercise — you probably want auth and payments handled for you. Beag gives you both in a single integration. Login, signup, password reset, and Stripe billing all work out of the box.

Here’s the setup:

1. Add the Beag Script

---
// src/layouts/BaseLayout.astro
---

<html>
  <head>
    <script
      src="https://app.beag.io/beag.js"
      data-site-id="your-site-id"
    ></script>
  </head>
  <body>
    <slot />
  </body>
</html>
---
// src/components/AuthButtons.astro
---

<nav>
  <a href="https://app.beag.io/login?site=your-site-id">Log in</a>
  <a href="https://app.beag.io/signup?site=your-site-id">Sign up</a>
</nav>

3. Check Auth State in Your Pages

After login, Beag stores user data in localStorage. You can check it client-side:

---
// src/pages/dashboard.astro
---

<html>
  <body>
    <div id="dashboard">
      <p>Loading...</p>
    </div>

    <script>
      const user = JSON.parse(localStorage.getItem('beag_user') || 'null');

      if (!user) {
        window.location.href = '/';
      } else {
        document.getElementById('dashboard').innerHTML = `
          <h1>Welcome, ${user.email}</h1>
          <p>Plan: ${user.plan || 'free'}</p>
        `;
      }
    </script>
  </body>
</html>

4. Gate Features by Plan

function hasAccess(requiredPlan) {
  const user = JSON.parse(localStorage.getItem('beag_user') || '{}');
  const planHierarchy = ['free', 'starter', 'pro', 'enterprise'];
  const userLevel = planHierarchy.indexOf(user.plan || 'free');
  const requiredLevel = planHierarchy.indexOf(requiredPlan);
  return userLevel >= requiredLevel;
}

// Usage
if (hasAccess('pro')) {
  showProFeatures();
}

No middleware to configure. No SSR adapter required. No webhook handlers for Stripe events. Beag handles the server-side session validation and payment state — your Astro app stays mostly static, which is what Astro is best at.

This pairs well with AI-generated frontends from tools like Cursor, Bolt, or Lovable. If your app was vibe-coded into existence, adding a script tag is a lot less risky than restructuring your project for server-side rendering. We wrote more about that workflow in our guide to monetizing vibe-coded side projects.

Which Approach Should You Pick?

Build it yourself if:

  • You need server-side auth checks on every request (admin panels, multi-tenant apps)
  • You want full control over the auth flow and session storage
  • You’re already running Astro in SSR mode with a database

Use Beag if:

  • You want auth and payments working today, not next week
  • Your Astro site is mostly static and you’d rather not add an SSR adapter
  • You’re a solo dev or indie hacker validating an idea fast
  • You need Stripe billing without building webhook handlers

Common Mistakes When Adding Auth to Astro

Forgetting the adapter. Middleware only runs in SSR mode. Without an adapter, your middleware file does nothing and your cookies never get set. If you choose the DIY route, install an adapter first.

Using output: 'static' without per-page overrides. If you keep the default static output but forget to add export const prerender = false to your auth pages, those pages will be pre-rendered at build time and won’t have access to cookies or request data.

Not typing Astro.locals. Astro won’t error if you access Astro.locals.user without defining the type. But you also won’t get autocomplete or catch typos. Add the type definition in env.d.ts early.

Storing session tokens in localStorage without server validation. Client-side tokens can be forged. If you’re checking auth purely in the browser, anyone can set localStorage.beag_user to whatever they want. For sensitive operations, always validate on the server. Beag handles this — the script validates tokens against the Beag server before exposing user data to your app.

FAQ

Does Astro support authentication natively?

No. Astro doesn’t ship with a built-in auth system. You need to either build your own using middleware and API routes (as shown above), use a third-party library like Better Auth, or use an all-in-one service. Astro’s middleware API gives you the hooks to implement auth, but the logic is yours to write.

Do I need SSR to add auth to an Astro app?

It depends on your approach. If you’re doing server-side session validation with cookies and middleware, yes — you need output: 'server' and an adapter. If you’re using a client-side auth solution like Beag that handles sessions externally, your Astro site can stay fully static.

What happened to Lucia auth for Astro?

Lucia was deprecated in March 2025. The maintainers now recommend implementing session-based auth directly (which is what the middleware approach in this guide does) or using Better Auth, which has first-class Astro support. If you’re migrating from Lucia, the patterns are similar — you’re just writing the session management code yourself instead of using Lucia’s abstractions.

Can I add Stripe payments to an Astro app?

Yes. You can integrate Stripe directly using their Node.js SDK in Astro API routes, or use Beag to get auth and Stripe billing pre-configured. The direct route means building checkout sessions, webhook handlers, and subscription management yourself. Beag handles all of that so you can focus on your product. Check out our Stripe + React integration guide for more context on what the direct integration looks like.

Is Astro a good choice for SaaS apps?

Astro is excellent for content-heavy SaaS apps, marketing sites, and dashboards where most pages don’t need client-side interactivity. With SSR mode and middleware, it can handle auth and dynamic data just fine. For highly interactive UIs, you can embed React, Vue, or Svelte components inside Astro pages using Astro Islands — giving you the best of both worlds.


Ready to skip the auth plumbing? Add auth + payments to your Astro app in 5 minutes. Try Beag free for 7 days — no credit card required.

For more framework guides and tutorials, check out the Beag blog.

About the Author
Bank K.

Bank K.

Serial entrepreneur & Co-founder of Beag.io

Founder of Beag.io. Indie hacker building tools to help developers ship faster.

Ready to Make Money From Your SaaS?

Turn your SaaS into cash with Beag.io. Get started now!

Start 7-day free trial →