Modules and Resources
AppShell renders sidebar navigation, breadcrumbs, and handles routing based on a structured configuration of modules and resources (and sub-resources, sub-sub-resources, and so on).
Modules and Resources both share core interface. Of interest:
path: string- the path segmentcomponent: (props: ResourceComponentProps) => ReactNode- the component to render when the router navigates to that module/resource (optional — omitting it causes the path to return a 404 while child routes remain accessible)guards?: Guard[]- optional array of guard functions to control access based on permissions or feature flags
A trivial modules example:
import { defineModule, defineResource, pass, hidden } from "@tailor-platform/app-shell";
const appShellPropModule = [
defineModule({
path: "purchasing",
component: PurchasingLandingPage,
resources: [
defineResource({
path: "orders",
component: OrdersPage,
subResources: [
defineResource({
path: ":id",
component: OrderDetailPage,
}),
],
}),
defineResource({
path: "invoices",
component: PurchaseInvoicesPage,
// ...
}),
],
}),
defineModule({
path: "sales",
component: SalesLandingPage,
resources: [
defineResource({
path: "invoices",
component: SalesInvoicesPage,
// ...
}),
// ...
],
}),
// Module without component - /dashboard returns 404, child routes remain accessible
defineModule({
path: "dashboard",
resources: [
defineResource({
path: "overview",
component: DashboardOverview,
}),
defineResource({
path: "analytics",
component: DashboardAnalytics,
}),
],
}),
// Module with guards for access control
defineModule({
path: "reports",
component: ReportsPage,
resources: [reportsListResource],
guards: [
async ({ context, signal }) => {
const ok = await fetch("/api/me/permissions?scope=reports", {
signal,
}).then((r) => r.ok);
return ok ? pass() : hidden();
},
],
}),
];Produces the following navigation menu:
› Purchasing
- Orders
- Invoices
› Sales
- Invoices
...
› Reports (only visible if user has reports permission)Modules show as top-level menu items and resources are the sub-menu items. Sub-resources do not show up on the side navigation menu, but will be available via other navigation and show in breadcrumbs.
In the example above, clicking the 'Orders' menu item above will take you to /{basePath}/purchasing/orders and render the OrdersPage page. If you browse, either by direct request or via client-side navigation to /{basePath}/purchasing/orders/1234, AppShell will render OrderDetailPage with 'id' available via useParams
Providing a component for a Module is optional. If omitted, navigating directly to the module path returns a 404 response while its child resources remain fully accessible. This is useful when a path segment exists solely to group child routes under a shared URL prefix with no landing page of its own.
When a module has no component, its breadcrumb segment is rendered as plain non-clickable text (instead of a link), preventing navigation to a page that does not exist.
The same applies to resources: a defineResource() call without a component makes the resource path itself return 404 while its subResources remain accessible.
Guards on component-less modules and resources execute correctly. For example, a redirectTo() guard will fire as expected. If all guards return pass(), the route falls back to a 404 (since there is no component to render).
Read more about client-side navigation in AppShell apps
Route Guards
Both modules and resources support guards - an array of functions that control access based on permissions, feature flags, or any other criteria. Guards are executed in order and provide:
- Composability: Combine multiple guards for complex access logic
- Reusability: Share common guards across routes
- Semantic constraints: Clear
pass(),hidden(), orredirectTo()results
See the Route Guards documentation in the API reference for full details.
Guard Examples
Permission-based access:
import { defineModule, pass, hidden } from "@tailor-platform/app-shell";
const reportsModule = defineModule({
path: "reports",
component: ReportsPage,
resources: [reportsListResource],
guards: [
async ({ context, signal }) => {
const ok = await fetch("/api/me/permissions?scope=reports", {
signal,
}).then((r) => r.ok);
return ok ? pass() : hidden();
},
],
});Feature flag based:
import { pass, hidden } from "@tailor-platform/app-shell";
const betaFeaturesModule = defineModule({
path: "beta",
component: BetaFeaturesPage,
resources: [newFeatureResource],
guards: [
async ({ context }) => {
const enabled = await checkFeatureFlag("beta-features");
return enabled ? pass() : hidden();
},
],
});Tenant tier based:
import { pass, hidden } from "@tailor-platform/app-shell";
const billingModule = defineModule({
path: "billing",
component: BillingPage,
resources: [billingResources],
guards: [
async ({ context }) => {
const plan = await getCurrentTenantPlan();
return plan === "enterprise" ? pass() : hidden();
},
],
});Resource-level guards:
import { pass, hidden } from "@tailor-platform/app-shell";
defineResource({
path: "admin-settings",
component: AdminSettingsPage,
guards: [
async ({ context, signal }) => {
const user = await getCurrentUser({ signal });
return user.role === "admin" ? pass() : hidden();
},
],
});Reusable guards:
import { type Guard, pass, hidden, redirectTo } from "@tailor-platform/app-shell";
// Define 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();
};
// Use in multiple resources
defineResource({
path: "admin/users",
component: AdminUsersPage,
guards: [requireAuth, requireAdmin],
});When a module or resource is hidden via guards, it will:
- Not appear in navigation menus
- Not be accessible via direct URL navigation
- Not appear in CommandPalette search results