TanStack Start
The app framework conventions - file routing, server functions, server-first rendering, and the web app's route tree.
Africa OS apps are built on TanStack Start (not Next.js). This page covers the conventions the repository follows: file-based routing, server functions, server-first rendering, and the shared app patterns.
File-based routing
TanStack Start uses file-based routing, like TanStack Router. Files under src/routes/ become routes:
routes/
├── __root.tsx root layout (html, providers)
├── _authenticated.tsx layout group with an auth guard
├── _authenticated/index.tsx the launcher at "/"
├── _authenticated/$organization.tsx layout for "/:organization"
└── _authenticated/$organization/$application.tsx mounts an app
Conventions used in this repo:
- Layout groups are folders or files with a pathless prefix (
_authenticated) - the segment is not part of the URL but its layout applies to nested routes. - Dynamic segments use
$($organization,$application). - Guards live in
beforeLoad- the route’s data-loading hook. See Web app.
The route tree is code-generated with tsr generate (pnpm generate-routes in each app), and kept in sync by the dev server.
Server functions
Server functions are the RPC layer - the TanStack Start replacement for a REST API. They run server-side and are callable from the client with full types:
import { createServerFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server";
export const listProductsFn = createServerFn({ method: "GET" })
.validator((data: unknown) => schema.parse(data))
.handler(async ({ data }) => {
const headers = getRequestHeaders();
// ... run an Effect program through the request runtime
});
The rules every server function follows:
- Validate input with a Zod validator before anything else.
- Read headers via
getRequestHeaders()and pass them to the request runtime. - Run the Effect program through
runRequest/runRequestFolded- never hand-assemble session, org, or database. - Fold typed failures into serializable results - client code never sees thrown errors.
- Scope to the organization via the request scope (URL slug) whenever the call targets tenant data.
Server-first rendering
The repository prefers server components and server functions first. Data fetching happens server-side; client components are added only when explicitly needed (forms, sheets, live updates). This keeps business logic and tenancy enforcement server-side and the client bundle lean.
The shared app pattern
Every TanStack Start app in the repo follows the same skeleton:
src/
├── router.tsx createRouter + route tree import
├── routes/ file-based routes
├── components/ app-level components
└── features/ domain features (auth, applications, ...)
App-specific features live under src/features/<name>/ with services/ for server functions, hooks/ for Query hooks, and components/ for UI - the same feature structure the application packages use.
The web app’s guard
Authentication is enforced in the _authenticated group’s beforeLoad:
export const Route = createFileRoute("/_authenticated")({
beforeLoad: async () => {
// session check via a server function; redirect to /login when missing
},
});
Route loaders that need data call server functions, which run through the request runtime and therefore carry the full request context.
Next steps
- Data fetching - how server functions and Query compose.
- Web app - the full route tree in practice.