App shell pattern
Every authenticated page in app/ renders the same chrome: NavBar with
brand, ThemeToggle, and UserMenu. 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: { email; name; image; platformAdmin };
organization: { slug; name } | null;
memberships: Array<{ orgSlug; orgName; role }>;
sidebar?: React.ReactNode; // optional left rail
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>
);
}Sidebar slot #
Sections with a left nav (/settings, /admin) pass it via the
sidebar prop:
<AppShell ... sidebar={<SettingsSidebar />}>{children}</AppShell>The slot forwards to PageShell's sidebar — no need to bring your own
layout grid.
Templates #
Existing layouts to copy from:
app/app/agents/layout.tsx— no sidebarapp/app/settings/layout.tsx— with sidebarapp/app/admin/layout.tsx— with sidebarapp/app/knowledge/layout.tsx— no sidebar
Public pages #
Auth pages (/sign-in, /sign-up, /invite/[token], /authorize-device,
/auth/verify) keep their minimal chrome and don't use AppShell — they
have their own (auth) layout. Don't wrap them.