App shell pattern
Every authenticated page in app/ renders the same chrome: NavBar with
brand, ThemeToggle, UserMenu, and the persistent application sidebar. When you add a new authed route group,
wrap it in <AppShell>.
Why #
Without a shared shell, pages drift — /agents lost its UserMenu and
theme toggle for a while, which made navigating between sections feel
broken. One component owning the chrome means future widgets
(notification bell, command palette, search) get added once.
The component #
Lives at app/components/AppShell.tsx.
Client component (UserMenu + ThemeToggle are interactive).
interface AppShellProps {
user: { id; email; name; image; platformAdmin };
organization: { id; slug; name } | null;
memberships: Array<{ orgSlug; orgName; role }>;
sidebar?: React.ReactNode; // optional contextual links below the shared navigation
children: React.ReactNode;
}It takes session-derived props as inputs. Server components resolve
the session via getCurrentSession() and pass the bits AppShell needs.
Pattern #
Each route group has a layout.tsx that:
- Resolves the session (
getCurrentSession). - Redirects to
/sign-in?next=…if missing. - Renders
<AppShell>with the session and (optionally) a sidebar.
// app/app/<section>/layout.tsx
import { redirect } from "next/navigation";
import { getCurrentSession } from "@/lib/session";
import { AppShell } from "@/components/AppShell";
export default async function SectionLayout({ children }) {
const session = await getCurrentSession();
if (!session?.user || !session.organization) {
redirect("/sign-in?next=/<section>");
}
const { user, organization, memberships } = session;
return (
<AppShell user={user} organization={organization} memberships={memberships}>
{children}
</AppShell>
);
}Persistent navigation #
AppSidebar groups the existing destinations into Workspace, Company,
Settings, and Platform (platform administrators only). Settings and admin
layouts use the shared shell without adding a second sidebar; their existing
server authentication and role checks still control access.
Search filters navigation labels and paths. Stars add or remove favourites. The collapse button switches the 224px sidebar to a 56px rail; links retain accessible names and hover labels. Preferences are stored only in this browser, under a key scoped to the current user and organization. They are not synced between devices. Unknown destinations and malformed preferences are ignored; navigation remains usable when browser storage is unavailable.
The optional sidebar slot adds contextual links below this shared navigation.
Templates #
Existing layouts to copy from:
app/app/agents/layout.tsxapp/app/settings/layout.tsxapp/app/admin/layout.tsx— retains its platform-admin guardapp/app/knowledge/layout.tsx
Public pages #
Entry pages (/sign-in, /sign-up, /invite/[token], /auth/verify)
keep their minimal chrome. Don't wrap them.
/maestro has its own server layout and uses AppShell when the existing
session resolver returns a user, including users without an organization.
Without a valid session it retains the minimal layout and existing client
sign-in return path. /authorize-device renders AppShell after its existing
server session guard; device approval and organization checks are unchanged.
/connect/slack wraps only its signed-in branch in AppShell, including users
without an organization. Its logged-out entry, fragment handoff, preview and
confirmation remain owned by the unchanged LinkSlackIdentity component.
The route audit covers home, agents, knowledge, workspaces, settings, admin, Maestro, device authorization and Slack linking. Nested pages inherit their section layout; the entry pages listed above remain minimal.
Mobile navigation and long labels #
Below the desktop breakpoint, a native disclosure below the header opens the
same searchable navigation with full labels and favourites. It closes when a
destination is selected and scrolls within a bounded height. A saved desktop
collapse preference never hides mobile labels. Optional section-specific
mobileNavigator controls still render after it.
The header hides the organization slug below sm, truncates it at larger widths, and bounds the account trigger so long names cannot displace controls. Account menus retain their full accessible label. NavBar allows the brand slot to shrink while reserving space for end controls.