All routes are public until they opt into auth. Add protected exact paths or URL prefixes in src/config/auth.ts, or check Astro.locals.isAuthenticated inside a specific page or route handler.

VK loads auth state for every request, but routes are public by default. The middleware in src/middleware.ts reads the Better Auth session, writes Astro.locals.user, Astro.locals.session, and Astro.locals.isAuthenticated, then asks src/auth/routes.ts whether the current URL requires a login redirect. Route policy values live in src/config/auth.ts.

Use src/config/auth.ts for route rules that should be enforced consistently by middleware. Use a route-local check when the rule is specific to one page or API handler.

The boilerplate also ships with app roles powered by the Better Auth admin plugin: admin, moderator, user, and banned. Admin URL routes are reserved for users with the app:administer permission.

Better Auth plugins are configured in src/auth/server.ts and src/auth/client.ts.

Better Auth Plugins

The Better Auth admin plugin is installed and configured. Server-side Better Auth plugins live in the plugins array in src/auth/server.ts; the matching browser client plugins live in the plugins array in src/auth/client.ts.

When adding or modifying a Better Auth plugin, check the full plugin surface:

  • src/auth/server.ts for server plugin imports, plugin options, database hooks, and runtime config used by Better Auth.
  • src/auth/client.ts for matching client plugin imports and client-side options.
  • src/config/auth.ts for app roles and app permission values.
  • src/auth/permissions.ts for Better Auth access-control construction and role normalization.
  • src/db/schema/auth.ts and drizzle/d1/* migrations for plugin-required tables or columns.
  • src/env.d.ts when the plugin changes the session or user fields exposed on Astro.locals.
  • tests/auth/server-config.test.ts, tests/auth/auth-schema.test.ts, and tests/auth/permissions.test.ts for plugin config, schema, and permission coverage.

Middleware-Protected Routes

Add exact URLs to protectedExactPaths when one route needs authentication:

export const authRouteConfig = {
  protectedExactPaths: ['/dashboard', '/account'],
  protectedPrefixes: [],
  // ...
} as const;

Unauthenticated requests to those paths redirect to /login with the original destination preserved:

/login?redirectTo=%2Fdashboard

Add URL prefixes to protectedPrefixes when a group of routes shares the same auth requirement:

export const authRouteConfig = {
  protectedExactPaths: ['/dashboard'],
  protectedPrefixes: ['/settings/', '/api/account/'],
  // ...
} as const;

Use slash-terminated prefixes when matching a route group. A prefix such as /settings/ protects /settings/profile without also matching unrelated paths like /settings-public. If the group index route should also be protected, add it as an exact path:

export const authRouteConfig = {
  protectedExactPaths: ['/dashboard', '/settings'],
  protectedPrefixes: ['/settings/'],
  // ...
} as const;

Astro filesystem route groups, such as src/pages/(app)/dashboard.astro, do not appear in request URLs. Add the URL path that the group produces, such as /dashboard, or a shared URL prefix used by the pages in that group.

Admin Routes

/admin and /admin/* are protected separately from general authenticated routes. Anonymous users are redirected to /login; authenticated users without the app:administer permission receive a 403 response.

Change admin route policy in src/config/auth.ts:

export const authRouteConfig = {
  adminExactPaths: ['/admin'],
  adminPrefixes: ['/admin/'],
  adminPermission: { app: ['administer'] },
  // ...
} as const;

Change role permissions in src/config/auth.ts:

export const authRoleConfig = {
  roleAppPermissions: {
    admin: ['access', 'moderate', 'administer'],
    moderator: ['access', 'moderate'],
    user: ['access'],
    banned: [],
  },
  // ...
} as const;

Route-Local Checks

Per-route auth is useful when a route needs custom behavior, conditional access, or a JSON 401 response instead of a middleware login redirect. Middleware still populates locals, so the route can decide for itself.

For a page, redirect from the page frontmatter:

---
const destination = `${Astro.url.pathname}${Astro.url.search}`;

if (!Astro.locals.isAuthenticated) {
  return Astro.redirect(
    `/login?redirectTo=${encodeURIComponent(destination)}`,
  );
}
---

For an API route, return an API-shaped response:

import type { APIRoute } from 'astro';
import { jsonFailure, jsonSuccess } from '@/lib/http/json';

export const POST: APIRoute = async ({ locals }) => {
  if (!locals.isAuthenticated) {
    return jsonFailure('Unauthorized', { status: 401 });
  }

  return jsonSuccess({ ok: true });
};

This is the right shape for one-off tools and diagnostics, including routes that allow either an authenticated session or a route-specific secret. Keep that logic inside the route when it should not apply globally.

Choosing A Pattern

Use protectedExactPaths for single pages like /dashboard.

Use protectedPrefixes for URL namespaces like /settings/ or /api/account/. Use the admin route policy for /admin and /admin/*.

Use route-local checks when the response should be custom, especially for API routes that should return 401 JSON instead of redirecting to the login page.

Use userHasAppPermission for local role checks:

import { userHasAppPermission } from '@/auth/permissions';

if (!userHasAppPermission(locals.user, { app: ['moderate'] })) {
  return new Response('Forbidden', { status: 403 });
}

Keep Better Auth endpoints under /api/auth public. Sign in, sign up, session, callback, verification, reset, and sign-out requests must be able to reach Better Auth before a user has an authenticated session.

Tests

When changing middleware-protected route policy, update the route-policy tests:

npm run test -- tests/auth tests/middleware

When adding route-local auth, test the route handler directly and pass the expected locals.isAuthenticated value in the route context.