---
title: Building an application
description: 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](/applications/anatomy-of-an-app) and the [retail reference](/applications/retail-reference) in mind - the goal is to mirror retail.

### What you are creating

A host-agnostic package in `packages/applications/<app>` that:

1. Self-describes through `app.config.ts`.
2. Exports a mountable root component.
3. Ships its domain as feature folders.
4. Registers with the platform registry.
5. Mounts in the web app.

### Step 1 - scaffold the package

Create `packages/applications/<app>/` with the standard files, modeled on retail:

```txt
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/
```

1. **Write package.json**

    Depend on the shell, ui, config, database, permissions, request-context, logger, and the TanStack packages, all as `workspace:*`:

    ```json
    {
      "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"
      }
    }
    ```

2. **Write app.config.ts**

    Declare the application's identity and feature surface with `defineAppConfig` (see [Configuration](/platform/configuration) for the contract):

    ```ts
    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: [],
    });
    ```

3. **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`.

4. **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

```bash
pnpm install
pnpm db:register-apps
```

The registry upserts the application from `app.config.ts`. Verify it with:

```bash
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`:

```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](/guides/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** - `CurrentOrganization` and the `orgWhere`/`orgScope` helpers keep data isolated.
- **Authorization** - `requirePermission` guards every domain action; the `platform.applications.access` gate runs automatically.
- **The shell** - sidebar chrome and navigation from `@africaos/shell` or your own `RetailShell`-style wrapper.
- **The launcher** - the dashboard renders your application from registry metadata, with no launcher code changes.

### Verification checklist

- [ ] `app.config.ts` passes `defineAppConfig` and registers without warnings.
- [ ] `pnpm nx run @africaos/<app>:typecheck` passes.
- [ ] 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 test` passes for the new package.

### Next steps

- [Adding a feature](/guides/adding-a-feature) - build the first domain slice.