Skip to content
Secure Mesh Docs
Esc
navigateopen⌘Jpreview
On this page

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:

  1. Bootstrapping before hydration. The root layout injects an inline theme script into <head> that reads the sm_theme cookie and applies the dark/light class to <html> before first paint, eliminating theme flash. The same theme is resolved server-side via getThemeFromCookies() so the server-rendered markup already carries the correct class.

  2. 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.

  3. Providers that survive navigation. RealtimeProvider and GlobalCallProvider are 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 TooltipProvider from @/components/ui/tooltip, making tooltip state available globally (app/layout.tsx).
  • (app) shell → providers → NavSidebar: the shell is RealtimeProvider > 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 mount RealtimeProvider or NavSidebar, keeping the pre-auth surface minimal.
  • Profile seeding: the server layout resolves UserProfile from /auth/me before rendering, so the sidebar receives userProfile as 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):

  1. Loads Inter via next/font/google with the CSS variable --font-sans, applied on the <html> element.
  2. Resolves the theme server-side with getThemeFromCookies() from @/lib/theme-server and computes isDark (theme !== 'light' ? 'dark' : 'light'), so the class is correct in server-rendered HTML.
  3. Injects the theme init script into <head> (see below) to guarantee the class is applied before React hydrates.
  4. Wraps all children in TooltipProvider inside a flex-column body that uses the design-token CSS variables --background and --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_theme cookie with a regex match ((?:^|; )sm_theme=([^;]*)), defaulting to dark.
  • A value of system is resolved through window.matchMedia('(prefers-color-scheme: dark)').
  • It mutates document.documentElement (the <html> element) class list directly — this is why <html> carries suppressHydrationWarning (app/layout.tsx): the client may legitimately diverge from the server-rendered class attribute when the user’s saved theme is system.
  • The entire body is wrapped in try/catch with a dark fallback, 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:

  1. The fetch is wrapped in try/catch (_) {} — a failed or unauthenticated request silently yields profile = null instead 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.
  2. 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 into UserProfile — a cheap type-narrowing that keeps the server boundary honest.
  3. The layout is async — Next.js awaits the fetch before streaming the shell, so the sidebar’s userProfile prop 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:

  • RealtimeProvider is the outermost provider — it owns the streaming/WebSocket connection for the whole session.
  • GlobalCallProvider sits inside it — call state (WebRTC) can depend on realtime signaling but must not outlive it.
  • Both wrap a fixed h-screen flex frame: NavSidebar on the left and a flex-1 content region containing {children} (the routed page). overflow-hidden and select-none enforce 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.

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 that apiService (and the shell’s /auth/me fetch) 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.tsxNavSidebar 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/me returns 401/403, the try/catch in the (app) layout swallows the error and renders the shell with profile = 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 to UserProfile; the shell falls back to the anonymous state instead of crashing.
  • Theme cookie corruption. The inline init script wraps everything in try/catch and falls back to dark on any exception; decodeURIComponent failures on malformed cookies are also contained (app/layout.tsx).
  • Hydration mismatch on system theme. Because the inline script may set dark or light differently from the server-rendered class when the preference is system, <html> carries suppressHydrationWarning (app/layout.tsx). Without it, React would warn (and potentially error) on first hydration.
  • Provider lifecycle vs. route changes. RealtimeProvider and GlobalCallProvider are 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/me fetch 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/me directly 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 for dark/light (only system may 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 children slot 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 outside RealtimeProvider depending on whether it needs realtime signaling, exactly as GlobalCallProvider demonstrates.
  • Adding a new authenticated section. Create a new folder under app/(app)/ with a page.tsx (e.g., app/(app)/archive/page.tsx); it automatically inherits the shell, sidebar, and providers with no changes to the shell. Add a layout.tsx in that folder only if the section needs persistent chrome across list/detail pages, as chats and groups do.
  • Active navigation state. NavSidebar receives activePage=""; wiring the current route into this prop (via usePathname in a client wrapper) is the intended seam for highlighting the active section without touching the shell structure.
  • Theme surface. The theme contract (sm_theme cookie + getThemeFromCookies + init script) is the extension point for adding themes or a theme switcher; the shell only ever sets dark/light classes on <html>.

Was this page helpful?