Web app
The @africaos/web app - its routes, guards, shell components, and server functions, and how they compose into the product surface.
@africaos/web is the main product surface: the unified shell where users sign in, manage their organizations, and use application modules. This page documents its structure and the code that makes it work.
The route tree
src/routes/
├── __root.tsx root layout - providers, theme, router outlet
├── index.tsx landing/redirect when unauthenticated
├── login.tsx sign-in
├── onboarding.tsx organization onboarding
├── _authenticated.tsx guard: requires a session
├── _authenticated/index.tsx the launcher (application cards)
├── _authenticated/$organization.tsx organization shell (sidebar + header)
└── _authenticated/$organization/$application.tsx mounts an application
The auth guard
_authenticated.tsx enforces authentication before any protected page renders:
export const Route = createFileRoute("/_authenticated")({
beforeLoad: async () => {
const session = await getSessionFn();
if (!session) {
throw redirect({ to: "/login" });
}
},
});
getSessionFn is a server function backed by AuthService.getSession. Unauthenticated visitors are redirected to /login.
The organization shell
routes/_authenticated/$organization.tsx renders the shell for one tenant:
export const Route = createFileRoute("/_authenticated/$organization")({
beforeLoad: async ({ params }) => {
const organization = await resolveOrganizationFn({
data: { organizationSlug: params.organization },
});
if (!organization) {
throw redirect({ to: "/" }); // not an active membership → first org
}
},
component: OrganizationLayout,
});
function OrganizationLayout() {
const { organization } = useParams({ from: Route.id });
return (
<SidebarProvider>
<AppSidebar organizationSlug={organization} />
<SidebarInset>
<Header />
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<Outlet />
</div>
</SidebarInset>
</SidebarProvider>
);
}
Two things matter:
beforeLoadverifies the slug server-side.resolveOrganizationFnruns the request scope, which requires the organization to exist and be an active membership. The shell never trusts the URL segment alone.- The layout composes the sidebar and header from
@africaos/ui’sSidebarset.
The launcher
routes/_authenticated/index.tsx is the dashboard. It calls a server function that lists the enabled applications for the active organization (RegistryService.listForOrganization) and renders a card per application from registry metadata - icon, color, name, description, route. It is fully data-driven.
The application route
routes/_authenticated/$organization/$application.tsx mounts an application:
const Mount = resolveApplicationMount(applicationSlug);
return Mount ? <Mount organizationSlug={organizationSlug} /> : <NotAvailableState />;
resolveApplicationMount looks the slug up in applicationMounts (see Application registry). Applications with no mount render the “not available yet” state.
Server functions
The web app keeps its server functions under src/features/<name>/services/:
features/auth/services/auth.functions.ts- session, sign-in, sign-up, sign-out.features/organizations/services/org.functions.ts- resolve organization, list organizations, create.features/applications/...- launcher data.
Every function runs through the request runtime (runRequest/runRequestFolded), so each call authenticates, resolves the tenant, and carries permissions.
The app sidebar and header
AppSidebar(src/components/app-sidebar.tsx) - brand, organization switcher, navigation. Links point into the current tenant (/${organizationSlug}/...).Header(src/components/header.tsx) - page chrome and actions.
Both are composed from @africaos/ui primitives and stay thin.
The composition rule
The web app is the composition root. It wires the request context, auth, organizations, the application registry, and the application mounts together. Application modules never import the web app - they are mounted by it.
Next steps
- Unified shell - the architecture of this app.
- Data fetching - how its server functions and hooks work.