Route Authentication
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.tsfor server plugin imports, plugin options, database hooks, and runtime config used by Better Auth.src/auth/client.tsfor matching client plugin imports and client-side options.src/config/auth.tsfor app roles and app permission values.src/auth/permissions.tsfor Better Auth access-control construction and role normalization.src/db/schema/auth.tsanddrizzle/d1/*migrations for plugin-required tables or columns.src/env.d.tswhen the plugin changes the session or user fields exposed onAstro.locals.tests/auth/server-config.test.ts,tests/auth/auth-schema.test.ts, andtests/auth/permissions.test.tsfor 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=%2FdashboardAdd 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/middlewareWhen adding route-local auth, test the route handler directly and pass the
expected locals.isAuthenticated value in the route context.