Application Shell & Routing
Application Shell & Routing
The web frontend of Secure Mesh (“Obsidian Precision E2EE Protocol”) is a Next.js 16 App Router application. The application shell — composed of the root layout, the authenticated (app) route-group layout, and the URL route tree — defines how every page is mounted, how the realtime/call providers are kept alive across navigation, and how theming and authentication are bootstrapped before first paint.
Purpose and Scope
This page documents the application shell and routing layer of streaming-frontend:
- The root layout (
app/layout.tsx) that bootstraps the HTML document, fonts, CSS variables, and theme initialization. - The authenticated shell (
app/(app)/layout.tsx) that fetches the user profile server-side and composes the sidebar, realtime, and call providers. - The route tree: route groups
(app)and(auth), nested layouts, dynamic segments ([peerId],[id]), server actions, and the auth token API route.
Related topics intentionally left to sibling pages: the individual feature pages (chats, groups, calls, settings, status), the NavSidebar component itself, the RealtimeProvider/GlobalCallProvider internals, and the apiService client. This page only covers how they are wired into the shell.
Overview
The shell solves three fundamental problems for a realtime messenger SPA:
-
Bootstrapping before hydration. The root layout injects an inline theme script into
<head>that reads thesm_themecookie and applies thedark/lightclass to<html>before first paint, eliminating theme flash. The same theme is resolved server-side viagetThemeFromCookies()so the server-rendered markup already carries the correct class. -
One shell, many routes. Route groups (
(app),(auth)) keep URLs clean while separating page contexts: authenticated pages share the full shell (sidebar + providers), while login/signup render without it. Nested layouts (chats/layout.tsx,groups/layout.tsx) scope section-level UI. -
Providers that survive navigation.
RealtimeProviderandGlobalCallProviderare mounted once at the(app)shell level, so WebSocket/streaming and WebRTC call state are preserved across route transitions inside the App Router.
The shell is deliberately server-rendered: the profile fetch happens in an async server layout, and each page underneath it is a server component unless it opts into client interactivity.
Architecture
The following diagram shows how the root layout wraps the two route groups, how the (app) group composes the shell providers, and how feature pages mount inside the shell.
Key relationships, all verified in source:
- Root layout → TooltipProvider → children: every page in the application, including the auth pages, is wrapped by
TooltipProviderfrom@/components/ui/tooltip, making tooltip state available globally (app/layout.tsx). (app)shell → providers → NavSidebar: the shell isRealtimeProvider > GlobalCallProvider > div > NavSidebar + children, so realtime and call state outlive individual page mounts (app/(app)/layout.tsx).(auth)group is outside the shell: login/signup never mountRealtimeProviderorNavSidebar, keeping the pre-auth surface minimal.- Profile seeding: the server layout resolves
UserProfilefrom/auth/mebefore rendering, so the sidebar receivesuserProfileas a prop without client-side fetching (app/(app)/layout.tsx).
Route Tree and Layout Hierarchy
The application uses the Next.js App Router (Next.js ^16.3.0-preview.10, React ^19.2.8, per package.json). The file system under app/ is the routing table. The verified route tree is:
| URL pattern | File | Segment type | Role |
|---|---|---|---|
/ |
app/page.tsx |
page | Root entry page |
/login |
app/(auth)/login/page.tsx |
page (group (auth)) |
Unauthenticated login |
/signup |
app/(auth)/signup/page.tsx |
page (group (auth)) |
Unauthenticated signup |
/chats |
app/(app)/chats/page.tsx |
page | Chats list |
/chats/[peerId] |
app/(app)/chats/[peerId]/page.tsx |
dynamic page | 1 conversation |
/chats (layout) |
app/(app)/chats/layout.tsx |
layout | Chats section chrome |
/groups |
app/(app)/groups/page.tsx |
page | Groups list |
/groups/[id] |
app/(app)/groups/[id]/page.tsx |
dynamic page | Group detail |
/groups (layout) |
app/(app)/groups/layout.tsx |
layout | Groups section chrome |
/calls |
app/(app)/calls/page.tsx |
page | Calls surface |
/settings |
app/(app)/settings/page.tsx |
page | Settings |
/status |
app/(app)/status/page.tsx |
page | Status feed |
/status/[id] |
app/(app)/status/[id]/page.tsx |
dynamic page | Status detail |
POST /api/auth/token (implied) |
app/api/auth/token/route.ts |
route handler | Token endpoint |
| — | app/actions/auth.ts |
server actions | Auth mutations |
Route groups (app) and (auth) are URL-neutral: parentheses remove the segment from the URL while still creating a layout boundary. This is the core routing design — the (app) group gets the full shell (sidebar, realtime, calls), the (auth) group does not, and both share the root layout.
Design intent. The route-group split keeps authentication concerns out of the shell: /login and /signup are rendered by the root layout alone, so no sidebar or realtime socket is created before authentication. Dynamic segments ([peerId], [id]) allow single-page navigation between list and detail views, which matters for a messenger where switching conversations must not tear down the RealtimeProvider mounted in the shell above.
Root Layout: Document Bootstrap
app/layout.tsx is an async server component that produces the HTML document. It does four jobs, in order (app/layout.tsx):
- Loads Inter via
next/font/googlewith the CSS variable--font-sans, applied on the<html>element. - Resolves the theme server-side with
getThemeFromCookies()from@/lib/theme-serverand computesisDark(theme !== 'light' ? 'dark' : 'light'), so the class is correct in server-rendered HTML. - Injects the theme init script into
<head>(see below) to guarantee the class is applied before React hydrates. - Wraps all children in
TooltipProviderinside a flex-column body that uses the design-token CSS variables--backgroundand--on-surface.
const themeInitScript = `(function(){try{var c=document.cookie.match(/(?:^|; )sm_theme=([^;]*)/);var t=c?decodeURIComponent(c[1]):'dark';var dark=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);var r=document.documentElement;if(dark){r.classList.add('dark');r.classList.remove('light');}else{r.classList.remove('dark');r.classList.add('light');}}catch(e){document.documentElement.classList.add('dark');}})();`;
Source: app/layout.tsx
The script is a classic flash-of-unstyled-theme (FOUT) prevention pattern:
- It reads the
sm_themecookie with a regex match ((?:^|; )sm_theme=([^;]*)), defaulting todark. - A value of
systemis resolved throughwindow.matchMedia('(prefers-color-scheme: dark)'). - It mutates
document.documentElement(the<html>element) class list directly — this is why<html>carriessuppressHydrationWarning(app/layout.tsx): the client may legitimately diverge from the server-rendered class attribute when the user’s saved theme issystem. - The entire body is wrapped in
try/catchwith adarkfallback, so even a cookie corruption or script error cannot produce a light flash on a dark-themed app.
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const theme = await getThemeFromCookies();
const isDark = theme !== 'light' ? 'dark' : 'light';
return (
<html lang="en" suppressHydrationWarning className={`h-full ${isDark} antialiased ${inter.variable}`}>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
</head>
<body className="min-h-full flex flex-col bg-[var(--background)] text-[var(--on-surface)] font-sans">
<TooltipProvider>{children}</TooltipProvider>
</body>
</html>
);
}
Source: app/layout.tsx
Design intent. There are two theme resolution paths (server cookie read + inline client script) because neither alone is sufficient: the server path makes the initial HTML correct for crawlers and no-JS clients, while the inline script handles the system preference correctly at the exact moment the DOM exists but before paint. The design tokens (--background, --on-surface) keep the shell decoupled from concrete color values — the theme only flips classes, and Tailwind v4 CSS variables in globals.css do the rest. The site metadata describes the product as “Secure Mesh | Obsidian Precision E2EE Protocol” / “Military-grade decentralized social messenger platform” (app/layout.tsx), reflecting the E2EE positioning reinforced by the @noble/curves cryptography dependency in package.json.
The Authenticated Shell: (app)/layout.tsx
The (app) group layout is the heart of the application shell. It is an async server component with three responsibilities: fetch the current user, mount the session-wide providers, and render the navigation chrome around the routed page.
import NavSidebar, { UserProfile } from '@/components/NavSidebar';
import RealtimeProvider from '@/components/RealtimeProvider';
import GlobalCallProvider from '@/components/calls/GlobalCallProvider';
import { apiService } from '@/lib/api/client';
export default async function AppLayout({ children }: { children: React.ReactNode }) {
let profile: UserProfile | null = null;
try {
const res = await apiService.get<UserProfile>('/auth/me');
if (res && typeof res === 'object' && !Array.isArray(res) && 'id' in res) {
profile = res;
}
} catch (_) {}
return (
<RealtimeProvider>
<GlobalCallProvider>
<div className="h-screen flex bg-[var(--background)] text-[var(--on-surface)] overflow-hidden select-none">
<NavSidebar activePage="" userProfile={profile} />
<div className="flex-1 flex overflow-hidden">
{children}
</div>
</div>
</GlobalCallProvider>
</RealtimeProvider>
);
}
Source: app/(app)/layout.tsx
Profile bootstrap with graceful degradation
The layout calls apiService.get<UserProfile>('/auth/me') during server rendering. Three defensive choices are notable:
- The fetch is wrapped in
try/catch (_) {}— a failed or unauthenticated request silently yieldsprofile = nullinstead of crashing the whole shell. The sidebar then renders in an anonymous state, and client-side logic (e.g.,RealtimeProvider) can react to the missing identity. - The response is shape-checked (
typeof res === 'object' && !Array.isArray(res) && 'id' in res) before assignment. This guards against an error payload or an array leaking intoUserProfile— a cheap type-narrowing that keeps the server boundary honest. - The layout is async — Next.js awaits the fetch before streaming the shell, so the sidebar’s
userProfileprop is populated in the initial HTML. There is no client-side “loading user” state, which avoids layout shift on every navigation.
Provider composition
The provider order matters:
RealtimeProvideris the outermost provider — it owns the streaming/WebSocket connection for the whole session.GlobalCallProvidersits inside it — call state (WebRTC) can depend on realtime signaling but must not outlive it.- Both wrap a fixed
h-screen flexframe:NavSidebaron the left and aflex-1content region containing{children}(the routed page).overflow-hiddenandselect-noneenforce an app-like, non-scrolling frame; inner panels scroll independently.
Because the shell renders above the children slot, navigating from /chats to /groups re-renders only the routed leaf — RealtimeProvider and GlobalCallProvider remain mounted, preserving socket connections, in-flight calls, and unread state. This is the primary reason the shell exists as a layout rather than a per-page wrapper.
Sidebar wiring
NavSidebar receives activePage="" and userProfile={profile}. activePage is currently an empty string, meaning the shell does not yet drive active-state highlighting from the router; the sidebar manages its own navigation surface (see the sibling page for NavSidebar internals). The UserProfile type is exported from @/components/NavSidebar itself, keeping the shell’s only data contract with the sidebar colocated.
Auth Surface: Server Actions and Token Route
The shell’s routing story is completed by two server-side entry points outside the page tree:
app/actions/auth.ts— Next.js server actions (mutations such as login/signup invoked from the(auth)pages).app/api/auth/token/route.ts— an API route handler exposing the token endpoint thatapiService(and the shell’s/auth/mefetch) ultimately depends on for authentication.
The (auth) group pages call these server actions to establish a session; after authentication the app navigates into the (app) group, where the shell’s /auth/me fetch validates the session server-side on every shell render. This two-surface design keeps credentials off the client bundle (server actions) while giving the API client a standard HTTP endpoint for token refresh.
Routing and Navigation Behavior
Layout persistence across dynamic segments
chats/layout.tsx and groups/layout.tsx wrap their list + detail pages, so /chats/:peerId and /groups/:id keep section chrome (e.g., a conversation list pane) mounted while only the detail pane swaps. Combined with the shell above, navigation between two chats never unmounts the realtime provider — a hard requirement for receiving messages in the background.
Shell-level route flow
The flow highlights the two rendering modes of the shell: server rendering for the first request (profile fetch, theme, HTML shell) and client-side navigation afterwards, where only the routed leaf re-renders inside the persistent shell frame.
Route tree summary
Every leaf in the Features group inherits the entire shell — the same h-screen frame, sidebar, and providers — which is what gives the product its consistent “app” feel across all authenticated screens.
Usage Examples
1. Bootstrapping the document with theme and fonts (root layout)
const themeInitScript = `(function(){try{var c=document.cookie.match(/(?:^|; )sm_theme=([^;]*)/);var t=c?decodeURIComponent(c[1]):'dark';var dark=t==='dark'||(t==='system'&&window.matchMedia('(prefers-color-scheme: dark)').matches);var r=document.documentElement;if(dark){r.classList.add('dark');r.classList.remove('light');}else{r.classList.remove('dark');r.classList.add('light');}}catch(e){document.documentElement.classList.add('dark');}})();`;
export const metadata: Metadata = {
title: "Secure Mesh | Obsidian Precision E2EE Protocol",
description: "Military-grade decentralized social messenger platform",
};
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
const theme = await getThemeFromCookies();
const isDark = theme !== 'light' ? 'dark' : 'light';
return (
<html lang="en" suppressHydrationWarning className={`h-full ${isDark} antialiased ${inter.variable}`}>
<head>
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
</head>
<body className="min-h-full flex flex-col bg-[var(--background)] text-[var(--on-surface)] font-sans">
<TooltipProvider>{children}</TooltipProvider>
</body>
</html>
);
}
Source: app/layout.tsx
What to notice: the server component reads the theme cookie for the initial HTML class, the inline script covers the system preference at the browser, suppressHydrationWarning permits the client/server class divergence, and TooltipProvider wraps everything so no page needs its own tooltip provider.
2. Composing the authenticated shell around routed children
export default async function AppLayout({ children }: { children: React.ReactNode }) {
let profile: UserProfile | null = null;
try {
const res = await apiService.get<UserProfile>('/auth/me');
if (res && typeof res === 'object' && !Array.isArray(res) && 'id' in res) {
profile = res;
}
} catch (_) {}
return (
<RealtimeProvider>
<GlobalCallProvider>
<div className="h-screen flex bg-[var(--background)] text-[var(--on-surface)] overflow-hidden select-none">
<NavSidebar activePage="" userProfile={profile} />
<div className="flex-1 flex overflow-hidden">
{children}
</div>
</div>
</GlobalCallProvider>
</RealtimeProvider>
);
}
Source: app/(app)/layout.tsx
What to notice: this is the canonical pattern for an authenticated app shell in the App Router — an async layout that seeds UI state server-side, a provider stack that must outlive route changes, and a children slot where routed leaves are swapped. The flex-1 flex overflow-hidden content region is the contract every feature page renders into.
3. Running the shell locally (package scripts)
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
Source: package.json
What to notice: the shell is a standard Next.js app — next dev for development with hot reload of layouts/routes, next build for production compilation, next start for serving. There is no custom server; the App Router handles the layout hierarchy and route matching natively.
Configuration Options
The shell is configured primarily through Next.js conventions and code rather than a config file. The verified configuration surface is:
| Option / Key | Location | Type | Default | Description |
|---|---|---|---|---|
sm_theme cookie |
app/layout.tsx + lib/theme-server |
cookie string | dark (when absent or on error) |
Theme preference: dark, light, or system; system resolves via prefers-color-scheme |
title / description metadata |
app/layout.tsx metadata |
string | “Secure Mesh | Obsidian Precision E2EE Protocol” / “Military-grade decentralized social messenger platform” | Document-level SEO metadata for every page under the root layout |
--font-sans variable |
app/layout.tsx (Inter from next/font/google) |
CSS variable | Inter, latin subset | Sans-serif font token applied to <html> and font-sans body |
--background / --on-surface |
app/layout.tsx body classes |
CSS variable (defined in globals.css) |
n/a | Design tokens used by shell and pages for theme-aware colors |
activePage prop |
app/(app)/layout.tsx → NavSidebar |
string | "" |
Currently unused active-page marker; sidebar drives its own state |
next dev / next build / next start |
package.json scripts |
script | n/a | Dev/build/start commands for the frontend |
Framework versions are pinned in package.json: next ^16.3.0-preview.10, react/react-dom ^19.2.8, tailwindcss ^4, @shadcn/react ^0.2.1, @base-ui/react ^1.6.0, zod ^4.4.3, and @noble/curves ^2.2.0 (E2EE primitives used by feature code, not the shell itself).
Failure Modes, Edge Cases & Concurrency
- Unauthenticated shell access. If
/auth/mereturns 401/403, thetry/catchin the(app)layout swallows the error and renders the shell withprofile = null(app/(app)/layout.tsx). The app does not hard-redirect at the layout level; protection is expected to be enforced by downstream features or the client. This is a deliberate degradation choice: the shell never renders an error page for auth failures. - Malformed profile payload. The shape check (
'id' in res) prevents a non-object or array response from being assigned toUserProfile; the shell falls back to the anonymous state instead of crashing. - Theme cookie corruption. The inline init script wraps everything in
try/catchand falls back todarkon any exception;decodeURIComponentfailures on malformed cookies are also contained (app/layout.tsx). - Hydration mismatch on
systemtheme. Because the inline script may setdarkorlightdifferently from the server-rendered class when the preference issystem,<html>carriessuppressHydrationWarning(app/layout.tsx). Without it, React would warn (and potentially error) on first hydration. - Provider lifecycle vs. route changes.
RealtimeProviderandGlobalCallProviderare mounted once in the shell, so they are not re-created on client-side navigation. Any code that assumes a fresh socket per page would be wrong; conversely, this is what keeps messaging uninterrupted. If a page needs a per-conversation connection, it must layer it inside the page rather than in the shell. - Server render cost. The shell performs a blocking
/auth/mefetch on every server render of any(app)route. Under high request concurrency this serializes first-byte time on the auth API; Next.js caches layouts across navigations, but a slow/auth/medirectly slows shell render.
Performance & Operational Considerations
- Single blocking server fetch. The
(app)layout awaits exactly one API call (/auth/me) before streaming; keep this endpoint fast and cacheable, as it gates the entire authenticated shell. - Zero client-side theme work. Theme resolution is done in
<head>before paint and via the server; no layout shift or post-hydration class flip occurs fordark/light(onlysystemmay differ, by design). - Provider permanence reduces reconnect churn. Keeping the realtime/call providers at the shell level avoids socket reconnects on navigation — a major win for battery, bandwidth, and message delivery latency in a streaming messenger.
- Static shell, dynamic leaves. Layouts and the sidebar render once per shell render; only the
childrenslot re-renders per navigation, so the shell cost is amortized across route transitions.
Extension Points
- Adding a shell-level provider. To add a new session-wide provider (e.g., presence or push registration), wrap it in the provider stack inside
(app)/layout.tsx— inside or outsideRealtimeProviderdepending on whether it needs realtime signaling, exactly asGlobalCallProviderdemonstrates. - Adding a new authenticated section. Create a new folder under
app/(app)/with apage.tsx(e.g.,app/(app)/archive/page.tsx); it automatically inherits the shell, sidebar, and providers with no changes to the shell. Add alayout.tsxin that folder only if the section needs persistent chrome across list/detail pages, aschatsandgroupsdo. - Active navigation state.
NavSidebarreceivesactivePage=""; wiring the current route into this prop (viausePathnamein a client wrapper) is the intended seam for highlighting the active section without touching the shell structure. - Theme surface. The theme contract (
sm_themecookie +getThemeFromCookies+ init script) is the extension point for adding themes or a theme switcher; the shell only ever setsdark/lightclasses on<html>.
Related Links
- NavSidebar component — the sidebar rendered by the
(app)shell, including theUserProfilecontract. - Realtime provider — the session-wide streaming provider mounted in the shell.
- Global call provider — the WebRTC call provider composed inside
RealtimeProvider. - API client —
apiService, used by the shell for the/auth/meprofile fetch. - Authentication —
app/actions/auth.tsserver actions andapp/api/auth/token/route.ts. - Source entry points: app/layout.tsx, app/(app)/layout.tsx, package.json.