Protected Routes
SvelteBolt provides a simple and flexible system for protecting routes that require user authentication. This system allows you to easily configure which routes should be protected and customize the redirect behavior for unauthenticated users.
Overview
The protected routes system consists of two main components:
- Global Route Protection - Configured in
src/lib/server/auth/protected-routes.ts
and enforced inhooks.server.ts
- Individual Route Guards - Utility functions for additional protection in specific routes
Global Route Protection
Configuration
All protected routes are configured in src/lib/server/auth/protected-routes.ts
:
export const protectedRoutes: ProtectedRouteConfig[] = [
{
path: "/dashboard",
redirectTo: "/auth/login",
includeRedirectTo: true,
},
// Add more protected routes here
{
path: "/admin",
redirectTo: "/auth/login",
includeRedirectTo: true,
},
{
path: "/profile",
redirectTo: "/auth/login",
includeRedirectTo: true,
},
];
Configuration Options
Option | Type | Default | Description |
---|---|---|---|
path | string | Required | The route pattern to protect (supports wildcards with startsWith logic) |
redirectTo | string | '/auth/login' | Custom redirect path for unauthenticated users |
includeRedirectTo | boolean | true | Whether to include the original path as redirectTo query parameter |
How It Works
- When a user visits any route, the
authGuard
inhooks.server.ts
checks if the route is protected - If the route is protected and the user is not authenticated, they are redirected to the login page
- The original URL is preserved as a query parameter so users can be redirected back after login
Examples
Basic Protection:
{
path: '/dashboard',
// Uses default redirect to '/auth/login' with redirectTo parameter
}
Custom Redirect:
{
path: '/admin',
redirectTo: '/auth/admin-login',
includeRedirectTo: false
}
Multiple Route Protection:
// Protects /dashboard, /dashboard/settings, /dashboard/profile, etc.
{ path: '/dashboard' },
// Protects only /profile (but not /profile/edit unless explicitly added)
{ path: '/profile' },
// Protects all admin routes
{ path: '/admin' }
Individual Route Guards
For additional protection or specific route logic, you can use route guard utilities in your +page.server.ts
or +layout.server.ts
files.
Basic Authentication Guard
import type { PageServerLoad } from "./$types";
import { requireAuth } from "$lib/server/auth/route-guards";
export const load: PageServerLoad = async (event) => {
// Ensure user is authenticated
requireAuth(event);
// Your route logic here
return {
user: event.locals.user,
};
};
Permission-Based Protection
import type { PageServerLoad } from "./$types";
import { requirePermission } from "$lib/server/auth/route-guards";
export const load: PageServerLoad = async (event) => {
// Require user to be admin
requirePermission(
event,
(user) => user.user_metadata?.role === "admin",
"/dashboard" // Redirect to dashboard if not admin
);
return {
adminData: "sensitive admin data",
};
};
Subscription-Based Protection
import type { PageServerLoad } from "./$types";
import { requireSubscription } from "$lib/server/auth/route-guards";
export const load: PageServerLoad = async (event) => {
// Require premium subscription
requireSubscription(
event,
"premium",
"/payment/upgrade" // Redirect to upgrade page
);
return {
premiumFeatures: "premium content",
};
};
Best Practices
1. Use Global Protection for Route Groups
Configure protection in protected-routes.ts
for entire route groups:
// Good: Protects all dashboard routes
{
path: "/dashboard";
}
// Instead of protecting each route individually
{
path: "/dashboard/profile";
}
{
path: "/dashboard/settings";
}
{
path: "/dashboard/billing";
}
2. Layer Protection for Sensitive Routes
Combine global protection with route guards for critical routes:
// In protected-routes.ts
{
path: "/admin";
}
// In /admin/+layout.server.ts
export const load: LayoutServerLoad = async (event) => {
requirePermission(event, (user) => user.user_metadata?.role === "admin");
return {};
};
3. Handle Subscription-Based Features
For SaaS applications, protect premium features:
// In protected-routes.ts - protect the entire premium section
{
path: "/premium";
}
// In specific premium routes
export const load: PageServerLoad = async (event) => {
requireSubscription(event, "premium");
return {};
};
4. Graceful Error Handling
The protection system automatically handles redirects, but you can customize error messages:
// Custom redirect with user-friendly message
requireAuth(event, "/auth/login?message=subscription-required");
Common Use Cases
Multi-Tenant Applications
export const protectedRoutes: ProtectedRouteConfig[] = [
{ path: "/app" }, // Main application
{ path: "/admin" }, // Admin panel
{ path: "/org" }, // Organization management
];
SaaS with Subscription Tiers
// Global protection
{
path: "/app";
}
// In premium feature routes
requireSubscription(event, "premium", "/payment/upgrade");
requireSubscription(event, "enterprise", "/payment/enterprise");
Role-Based Access
// Admin routes
requirePermission(event, (user) =>
["admin", "moderator"].includes(user.user_metadata?.role)
);
// User-specific content
requirePermission(
event,
(user) =>
user.id === event.params.userId || user.user_metadata?.role === "admin"
);
Testing Protected Routes
Manual Testing
- Visit a protected route while logged out
- Verify you're redirected to login
- Log in and verify you're redirected back to the original route
- Test with different user roles/subscriptions
Automated Testing
// Example test with Playwright
import { test, expect } from "@playwright/test";
test("protected route redirects to login", async ({ page }) => {
await page.goto("/dashboard");
await expect(page).toHaveURL("/auth/login?redirectTo=%2Fdashboard");
});
test("authenticated user can access dashboard", async ({ page }) => {
// Login logic here
await page.goto("/dashboard");
await expect(page).toHaveURL("/dashboard");
});
Troubleshooting
Route Not Protected
- Check that the route is added to
protectedRoutes
array - Verify the path pattern matches your route structure
- Ensure
hooks.server.ts
is properly configured
Infinite Redirect Loops
- Check that your redirect destination is not also protected
- Verify authentication state is properly set in
hooks.server.ts
- Ensure login route is accessible to unauthenticated users
Custom Redirect Not Working
- Verify the
redirectTo
path exists and is accessible - Check that
includeRedirectTo
is set correctly - Ensure URL parameters are properly encoded
Migration from Manual Protection
If you currently have manual route protection in individual files:
- Identify Protected Routes: List all routes that check
event.locals.session
- Add to Configuration: Add these routes to
protected-routes.ts
- Remove Manual Checks: Remove individual authentication checks
- Test Thoroughly: Verify all routes still work as expected
Before (manual protection):
// In +page.server.ts
export const load: PageServerLoad = async (event) => {
if (!event.locals.session) {
redirect(303, "/auth/login");
}
// ...
};
After (global protection):
// In protected-routes.ts
{
path: "/dashboard";
}
// In +page.server.ts (optional additional protection)
export const load: PageServerLoad = async (event) => {
// No manual auth check needed!
// ...
};
This system provides a clean, maintainable way to manage authentication across your SvelteBolt application while remaining flexible for complex authorization scenarios.