Building an application
The step-by-step recipe for adding a new application module to the platform - package, config, shell, feature, routes, registry, and mount.
This guide walks through adding a new application module, end to end. It assumes you have the anatomy and the retail reference in mind - the goal is to mirror retail.
What you are creating
A host-agnostic package in packages/applications/<app> that:
- Self-describes through
app.config.ts. - Exports a mountable root component.
- Ships its domain as feature folders.
- Registers with the platform registry.
- Mounts in the web app.
Step 1 - scaffold the package
Create packages/applications/<app>/ with the standard files, modeled on retail:
packages/applications/<app>/
├── app.config.ts
├── package.json @africaos/<app>, "private": true
├── tsconfig.json extends "@africaos/typescript-config/..."
├── project.json
├── vitest.config.ts
└── src/
├── index.ts
├── application.tsx
├── queryClient.ts
├── components/
├── features/
└── routes/
Write package.json
Depend on the shell, ui, config, database, permissions, request-context, logger, and the TanStack packages, all as workspace:*:
{
"name": "@africaos/<app>",
"private": true,
"dependencies": {
"@africaos/config": "workspace:*",
"@africaos/database": "workspace:*",
"@africaos/logger": "workspace:*",
"@africaos/permissions": "workspace:*",
"@africaos/request-context": "workspace:*",
"@africaos/shell": "workspace:*",
"@africaos/ui": "workspace:*",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-router": "latest",
"@tanstack/react-start": "latest",
"effect": "4.0.0-beta.103",
"react": "^19.2.0",
"zod": "4"
}
}Write app.config.ts
Declare the application’s identity and feature surface with defineAppConfig (see Configuration for the contract):
import { defineAppConfig } from "@africaos/config";
export default defineAppConfig({
id: "<app>",
name: "<App> OS",
icon: "package",
color: "#2563eb",
description: "Your vertical's purpose",
route: "/",
capabilities: { offline: false, desktop: true, mobile: false },
features: [],
});Write the mountable root
Export a <XApplication organizationSlug={...} /> component that computes the path relative to its mount prefix and renders the shell with the matching page - exactly like RetailApplication.
Expose the public surface
In src/index.ts, export the root component, the config, the query client, the route tree, and anything consumers need.
Step 2 - install and register
pnpm install
pnpm db:register-apps
The registry upserts the application from app.config.ts. Verify it with:
pnpm nx run @africaos/<app>:typecheck
Step 3 - mount it in the web app
Add one lazy entry to apps/web/src/features/applications/app-registry.tsx:
import { lazy, type ComponentType } from "react";
export const applicationMounts: Record<string, ComponentType<ApplicationMountProps>> = {
// ...existing
"<app>": lazy(() =>
import("@africaos/<app>").then((module) => ({
default: module.XApplication,
}))
),
};
The shell’s $application route will now render the application at /:organization/<app> whenever it is enabled for the organization and the user holds platform.applications.access.
Step 4 - enable it for an organization
The registry row alone does not enable the application for any organization. Enable it in the seed data (infra/database/seeds/development.sql) or through OrganizationService.enableApplication / RegistryService.setEnabled. The launcher only lists enabled applications.
Step 5 - build a feature
Mirror the retail products feature to add your first domain slice. The full recipe is on Adding a feature.
What you get for free
Because your application runs through the platform, you inherit:
- Authentication - every server function authenticates via the request runtime.
- Tenancy -
CurrentOrganizationand theorgWhere/orgScopehelpers keep data isolated. - Authorization -
requirePermissionguards every domain action; theplatform.applications.accessgate runs automatically. - The shell - sidebar chrome and navigation from
@africaos/shellor your ownRetailShell-style wrapper. - The launcher - the dashboard renders your application from registry metadata, with no launcher code changes.
Verification checklist
-
app.config.tspassesdefineAppConfigand registers without warnings. -
pnpm nx run @africaos/<app>:typecheckpasses. - The lazy mount resolves in
app-registry.tsx. - The application appears in the launcher once enabled for an org.
- Server functions scope to the active organization and guard with
requirePermission. -
pnpm testpasses for the new package.
Next steps
- Adding a feature - build the first domain slice.