Unified shell
How the web app composes authentication, the sidebar, the organization switcher, and application mounts into one navigable product surface.
The unified shell is what users actually see: the web app (apps/web) that composes every application module into one navigable product. It provides the chrome - sidebar, header, organization switcher - and mounts applications at /:organization/:application.
The route tree
The shell is built on TanStack Start file-based routing (apps/web/src/routes):
routes/
├── _authenticated.tsx auth guard: redirects unauthenticated users
├── _authenticated/index.tsx the launcher (application cards)
├── _authenticated/$organization.tsx org shell: sidebar + header + outlet
└── _authenticated/$organization/$application.tsx mounts an application
The _authenticated group is the shell’s front door. Its guard checks the session server-side before any shell page renders.
The route lifecycle
The organization shell
routes/_authenticated/$organization.tsx renders the shell for one organization:
beforeLoadcallsresolveOrganizationFnwith the slug, which runs the request scope server-side. If the slug is not one of the user’s active memberships, it redirects to/(which lands on the first organization).- The layout composes
SidebarProvider>AppSidebar+SidebarInset>Header+ anOutlet.
The slug is verified server-side, so the shell never trusts the URL segment on its own.
The sidebar and header
AppSidebar(apps/web/src/components/app-sidebar.tsx) renders the brand, the organization switcher, and navigation. It takes theorganizationSlugso links point into the current tenant.Header(apps/web/src/components/header.tsx) carries the page chrome and actions.
Both are built from @africaos/ui primitives - see Frontend.
The launcher
routes/_authenticated/index.tsx is the dashboard. It queries RegistryService.listForOrganization (through a server function) and renders one card per enabled application, using registry metadata - icon, color, name, description, route. Selecting a card navigates to /:organization/:application.
The launcher is fully data-driven. There is no application-specific code in it.
Mounting an application
The $application route resolves the mount from app-registry.tsx:
- If the slug has a lazy mount, it renders that component with
organizationSlugas the only prop. - If not, it renders the “not available yet” state.
Each application renders through ApplicationMount, which is host-aware: inside the web app (embedded mode) it renders content only, because the shell already provides the sidebar. See The shell package for how applications structure themselves.
Composition responsibilities
The web app is the composition root. It wires together:
getSessionFnand auth server functions from@africaos/auth.resolveOrganizationFnand org server functions from@africaos/organizations.- The request runtime from
@africaos/request-context(every server function runs through it). - The application mounts from
@africaos/*application packages. - The shell primitives from
@africaos/shell.
Application modules depend on the shell and platform packages, never on the web app. This keeps every vertical swappable.
Next steps
- Application anatomy - what an application module looks like inside.
- Web app (frontend) - the web app’s full structure.