Guards Overview
Guards are functions that control access to routes and UI elements in AppShell. They provide a flexible, composable way to implement permission-based and role-based access control.
How Guards Work
Guards are executed sequentially and can return one of three results:
pass()- Allow access, continue to next guardhidden()- Deny access, return 404redirectTo(path)- Redirect to another page
If any guard returns hidden() or redirectTo(), execution stops immediately.
Guard Function Signature
type Guard = (ctx: GuardContext) => GuardResult | Promise<GuardResult>;
type GuardContext = {
context: ContextData; // Your custom context from AppShell
};
type GuardResult = { type: "pass" } | { type: "hidden" } | { type: "redirect"; to: string };Guard Functions
pass()
Allows access and continues to the next guard.
import { pass } from "@tailor-platform/app-shell";
const allowAll: Guard = () => pass();hidden()
Denies access and shows 404 Not Found.
import { hidden } from "@tailor-platform/app-shell";
const denyAll: Guard = () => hidden();redirectTo(path)
Redirects to another page.
import { redirectTo } from "@tailor-platform/app-shell";
const redirectToLogin: Guard = () => redirectTo("/login");Usage Locations
Route-Level Guards
Applied to modules and resources:
import { defineModule, pass, hidden } from "@tailor-platform/app-shell";
const adminModule = defineModule({
path: "admin",
component: AdminPage,
guards: [
({ context }) => {
return context.currentUser?.role === "admin" ? pass() : hidden();
},
],
});Component-Level Guards
Applied to UI elements with WithGuard:
import { WithGuard, pass, hidden } from "@tailor-platform/app-shell";
const isAdmin = ({ context }) =>
context.currentUser?.role === "admin" ? pass() : hidden();
<WithGuard guards={[isAdmin]}>
<Button>Delete All</Button>
</WithGuard>Context Data
Guards receive context data from AppShell:
// Define your context type
declare module "@tailor-platform/app-shell" {
interface AppShellRegister {
contextData: {
currentUser: User | null;
permissions: string[];
featureFlags: FeatureFlags;
};
}
}
// Pass context to AppShell
<AppShell
modules={modules}
contextData={{
currentUser,
permissions,
featureFlags,
}}
>
{/* ... */}
</AppShell>
// Access in guards
const guard: Guard = ({ context }) => {
// context is fully typed!
if (context.currentUser) {
return pass();
}
return hidden();
};Common Patterns
Authentication Check
const requireAuth: Guard = ({ context }) => {
if (!context.currentUser) {
return redirectTo("/login");
}
return pass();
};Role-Based Access
const requireAdmin: Guard = ({ context }) => {
if (context.currentUser?.role !== "admin") {
return hidden();
}
return pass();
};Permission-Based Access
const requirePermission =
(permission: string): Guard =>
({ context }) => {
if (!context.permissions.includes(permission)) {
return hidden();
}
return pass();
};
// Usage
guards: [requirePermission("users:delete")];Feature Flags
const requireFeature =
(flag: string): Guard =>
({ context }) => {
if (!context.featureFlags[flag]) {
return hidden();
}
return pass();
};Async Permission Check
const checkApiPermission: Guard = async ({ context }) => {
const response = await fetch("/api/permissions/admin");
const { allowed } = await response.json();
return allowed ? pass() : hidden();
};Multiple Guards (AND logic)
const guards = [requireAuth, requireAdmin, requireFeature("beta")];
// All must pass() for access to be granted
defineModule({
path: "admin",
guards: guards,
});Reusable Guard Library
// guards.ts
export const requireAuth: Guard = ({ context }) => {
return context.currentUser ? pass() : redirectTo("/login");
};
export const requireRole =
(role: string): Guard =>
({ context }) => {
return context.currentUser?.role === role ? pass() : hidden();
};
export const requirePermission =
(perm: string): Guard =>
({ context }) => {
return context.permissions.includes(perm) ? pass() : hidden();
};
// Usage across application
import { requireAuth, requireRole } from "./guards";
defineModule({
path: "admin",
guards: [requireAuth, requireRole("admin")],
});Guard Execution Order
Guards are executed sequentially in the order they're defined:
guards: [guardA, guardB, guardC];
// Execution:
// 1. guardA runs → if not pass(), stop
// 2. guardB runs → if not pass(), stop
// 3. guardC runs → if not pass(), stop
// 4. All passed → grant accessNavigation Behavior
When guards return hidden():
- Route returns 404 Not Found
- Item hidden from sidebar navigation
- Item hidden from CommandPalette
- Breadcrumbs don't show the page
When guards return redirectTo():
- User is redirected immediately
- Original URL is not accessible
- Useful for login flows
Best Practices
Do:
- ✅ Keep guards simple and focused
- ✅ Reuse guards across routes and components
- ✅ Use descriptive names (requireAuth, not guard1)
- ✅ Return quickly for better performance
- ✅ Cache expensive checks when possible
Don't:
- ❌ Make guards too complex (extract logic)
- ❌ Have side effects in guards (logging OK)
- ❌ Duplicate guard logic (DRY principle)
- ❌ Forget to handle async errors
- ❌ Use redirectTo() in WithGuard (not supported)
TypeScript
Full type safety:
import { type Guard, type GuardContext, type GuardResult } from "@tailor-platform/app-shell";
const myGuard: Guard = (ctx: GuardContext): GuardResult => {
// Fully typed context
return pass();
};
// Async guard
const asyncGuard: Guard = async (ctx): Promise<GuardResult> => {
await someAsyncCheck();
return pass();
};Performance
Sync Guards (Fast)
const isAdmin: Guard = ({ context }) => {
return context.currentUser?.role === "admin" ? pass() : hidden();
};
// ~0ms - InstantAsync Guards (Slower)
const checkPermission: Guard = async ({ context }) => {
const allowed = await fetch("/api/check");
return allowed ? pass() : hidden();
};
// ~50-200ms - Network requestTip: Prefer sync guards when possible. Cache API results in context data.
Comparison: Route vs Component Guards
| Aspect | Route Guards | Component Guards (WithGuard) |
|---|---|---|
| Location | defineModule/defineResource | WithGuard component |
| Supports pass() | ✅ Yes | ✅ Yes |
| Supports hidden() | ✅ Yes | ✅ Yes |
| Supports redirectTo() | ✅ Yes | ❌ No |
| Execution | Before route loads | During render |
| Use Case | Page access control | UI element visibility |
Related
- pass() - Allow access guard function
- hidden() - Hide/deny access guard function
- redirectTo() - Redirect guard function
- WithGuard Component - Component-level guards
- Guards & Permissions Guide - Detailed tutorial