Skip to main content
Guards are functions that control access to modules and resources in your AppShell application. They execute before a route renders and can allow access, deny access (showing 404), or redirect to another path.

How Guards Work

Guards are executed:
  • Before route rendering: Guards run during the route’s loader phase
  • In order: Guards in the array are evaluated sequentially
  • Short-circuit on non-pass: The first guard that returns hidden() or redirectTo() stops execution
  • Once per navigation: Re-evaluated when navigating to the route

Guard Function Type

type Guard = (ctx: GuardContext) => Promise<GuardResult> | GuardResult;

type GuardContext = {
  context: ContextData;  // Custom context from AppShell
};

type GuardResult =
  | { type: "pass" }                  // Allow access
  | { type: "hidden" }                // Deny access (show 404)
  | { type: "redirect"; to: string }; // Redirect to path

Basic Usage

Module-Level Guards

Apply guards to an entire module and all its resources:
import { defineModule, pass, hidden, redirectTo } from "@tailor-platform/app-shell";

const adminModule = defineModule({
  path: "admin",
  component: AdminDashboard,
  resources: [usersResource, settingsResource],
  guards: [
    ({ context }) => {
      if (!context.currentUser) {
        return redirectTo("/login");
      }
      if (context.currentUser.role !== "admin") {
        return hidden();
      }
      return pass();
    }
  ]
});

Resource-Level Guards

Apply guards to individual resources:
import { defineResource, pass, hidden } from "@tailor-platform/app-shell";

const billingResource = defineResource({
  path: "billing",
  component: BillingPage,
  guards: [
    ({ context }) => {
      const isPremium = context.currentUser?.plan === "premium";
      return isPremium ? pass() : hidden();
    }
  ]
});

Composable Guards

Guards are composable - define reusable guard functions:
import { type Guard, pass, hidden, redirectTo } from "@tailor-platform/app-shell";

// Reusable guards
const requireAuth: Guard = ({ context }) => {
  if (!context.currentUser) {
    return redirectTo("/login");
  }
  return pass();
};

const requireAdmin: Guard = ({ context }) => {
  if (context.currentUser?.role !== "admin") {
    return hidden();
  }
  return pass();
};

const requirePlan = (plan: string): Guard => {
  return ({ context }) => {
    if (context.currentUser?.plan !== plan) {
      return hidden();
    }
    return pass();
  };
};

// Compose guards
defineModule({
  path: "admin",
  component: AdminDashboard,
  resources: [adminResources],
  guards: [requireAuth, requireAdmin]
});

defineResource({
  path: "premium-features",
  component: PremiumFeatures,
  guards: [requireAuth, requirePlan("premium")]
});

Async Guards

Guards can be async for permission checks or feature flags:
const checkPermission = async ({ context }): Promise<GuardResult> => {
  const hasAccess = await fetch("/api/permissions/reports")
    .then(r => r.ok);
  
  return hasAccess ? pass() : hidden();
};

defineModule({
  path: "reports",
  component: ReportsPage,
  resources: [reportsResources],
  guards: [checkPermission]
});

Accessing Context Data

Guards receive the context object from AppShell’s contextData prop:
// Define context type (in types/app-shell.d.ts)
declare module "@tailor-platform/app-shell" {
  interface AppShellRegister {
    contextData: {
      currentUser: User | null;
      apiClient: ApiClient;
      featureFlags: Record<string, boolean>;
    };
  }
}

// Pass context to AppShell
<AppShell
  modules={modules}
  contextData={{
    currentUser,
    apiClient,
    featureFlags
  }}
>
  <SidebarLayout />
</AppShell>

// Use in guards
const requireFeature = (feature: string): Guard => {
  return ({ context }) => {
    return context.featureFlags[feature] ? pass() : hidden();
  };
};

Guard Results

Guards must return one of three result types:

pass()

Allow access and continue to the next guard or render the component.

hidden()

Deny access and show a 404 Not Found page.

redirectTo(path)

Redirect the user to another path.

Effect on Navigation

When guards deny access:
  • Navigation: The route is not accessible via direct URL or client-side navigation
  • Sidebar: Hidden routes do not appear in navigation menus
  • Command Palette: Hidden routes are excluded from search results
  • 404 Behavior: hidden() returns a 404 response
  • Redirects: redirectTo() performs a client-side redirect

Common Patterns

Authentication Check

const requireAuth: Guard = ({ context }) => {
  if (!context.currentUser) {
    return redirectTo("/login");
  }
  return pass();
};

Role-Based Access

const requireRole = (role: string): Guard => {
  return ({ context }) => {
    if (context.currentUser?.role !== role) {
      return hidden();
    }
    return pass();
  };
};

Feature Flag

const requireFeature = (flag: string): Guard => {
  return ({ context }) => {
    return context.featureFlags[flag] ? pass() : hidden();
  };
};

Permission Check

const requirePermission = (permission: string): Guard => {
  return async ({ context }) => {
    const hasPermission = await context.apiClient.checkPermission(permission);
    return hasPermission ? pass() : hidden();
  };
};

Next Steps

Build docs developers (and LLMs) love