Skip to main content

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:

  1. Global Route Protection - Configured in src/lib/server/auth/protected-routes.ts and enforced in hooks.server.ts
  2. 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

OptionTypeDefaultDescription
pathstringRequiredThe route pattern to protect (supports wildcards with startsWith logic)
redirectTostring'/auth/login'Custom redirect path for unauthenticated users
includeRedirectTobooleantrueWhether to include the original path as redirectTo query parameter

How It Works

  1. When a user visits any route, the authGuard in hooks.server.ts checks if the route is protected
  2. If the route is protected and the user is not authenticated, they are redirected to the login page
  3. 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

  1. Visit a protected route while logged out
  2. Verify you're redirected to login
  3. Log in and verify you're redirected back to the original route
  4. 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

  1. Check that the route is added to protectedRoutes array
  2. Verify the path pattern matches your route structure
  3. Ensure hooks.server.ts is properly configured

Infinite Redirect Loops

  1. Check that your redirect destination is not also protected
  2. Verify authentication state is properly set in hooks.server.ts
  3. Ensure login route is accessible to unauthenticated users

Custom Redirect Not Working

  1. Verify the redirectTo path exists and is accessible
  2. Check that includeRedirectTo is set correctly
  3. Ensure URL parameters are properly encoded

Migration from Manual Protection

If you currently have manual route protection in individual files:

  1. Identify Protected Routes: List all routes that check event.locals.session
  2. Add to Configuration: Add these routes to protected-routes.ts
  3. Remove Manual Checks: Remove individual authentication checks
  4. 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.