---
title: Application registry
description: How applications self-describe through app.config.ts, register with RegistryService, and get mounted by the web app.
---

The application registry is how Africa OS knows which applications exist, what they look like, and which organizations can use them. Applications are not hardcoded into the platform - they self-describe through an `app.config.ts`, register their metadata, and the platform reads it back to build the launcher and mount modules.

### The three-sided relationship

```mermaid
flowchart LR
    App["app.config.ts<br/>application self-description"]
    Register["RegistryService.register<br/>upserts into platform.applications"]
    OrgApp["organization_applications<br/>which orgs can use it"]
    WebApp["web app launcher + mount"]

    App -->|register| Register
    Register --> OrgApp
    WebApp -->|listForOrganization| Register
    WebApp -->|lazy mount| App
```

- **Definition** - what the application is: slug, name, icon, color, description, route, offline support.
- **Enablement** - whether a specific organization can use it, tracked in `organization_applications`.
- **Mount** - how the web app renders it, wired in the app registry (`app-registry.tsx`).

### The application definition

`platform/organizations/src/metadata.ts` defines the contract every `app.config.ts` must satisfy:

```ts
export const applicationDefinitionSchema = z.object({
  slug: z.string().min(1),
  name: z.string().min(1),
  icon: z.string().optional(),          // a lucide icon name
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(), // #2563eb
  description: z.string().optional(),
  route: z.string().startsWith("/").optional(), // entry path, relative
  supportsOffline: z.boolean().default(false),
});
```

When read for a specific organization, the definition is extended with `enabled` - an organization-scoped fact, never part of the definition itself.

### Registering an application

Applications register through `RegistryService.register`, which validates the definition with the schema and upserts on slug:

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

The script reads every application package's `app.config.ts`, calls `register` for each, and stores the metadata in `platform.applications`. It is safe to run repeatedly - it refreshes metadata on conflict.

### Reading the registry

`RegistryService` offers:

| Method | Purpose |
| --- | --- |
| `listForOrganization(organizationId)` | The enabled applications for an org, as full metadata. This drives the launcher. |
| `listAll()` | Every registered application, no org context. The platform/admin view. |
| `getForOrganization(organizationId, slug)` | One enabled application by slug, with `ApplicationNotFound`/`ApplicationDisabled` failures. Used by the request runtime. |
| `setEnabled(organizationId, slug, enabled)` | Toggle whether an org can use an application. |
| `register(definition)` | Upsert an application definition. |

Tenant scoping is the caller's responsibility, with a hard rule: **`organizationId` must come from the request runtime (`CurrentOrganization`), never from client input.**

### The availability check

When a request is scoped to `:organization/:application/*`, the request runtime calls `getForOrganization`. The check has three gates:

1. The application exists in `platform.applications` (`ApplicationNotFound`).
2. It is enabled for the organization in `organization_applications` (`ApplicationDisabled`).
3. The user holds `platform.applications.access` (`Forbidden`).

This is the platform-level availability gate, resolved before any domain permission.

### Mounting applications

The web app keeps a lazy-mount map (`apps/web/src/features/applications/app-registry.tsx`) from application slug to component:

```ts
export const applicationMounts: Record<string, ComponentType<ApplicationMountProps>> = {
  retail: lazy(() =>
    import("@africaos/retail").then((module) => ({
      default: module.RetailApplication,
    }))
  ),
  agriculture: lazy(() => ...),
  // ...
};
```

Each mount is a `React.lazy` dynamic import, so every application is its own route-level chunk and the shell never pays for an application it does not open. Adding an application is one entry in this map plus its `app.config.ts`.

### The launcher

The dashboard launcher is fully data-driven: it calls `listForOrganization` and renders a card per application using the registry metadata (name, icon, color, description, route). Nothing about specific applications is hardcoded in the launcher - change the registry and the launcher follows.

### Next steps

- [Building an application](/applications/building-an-application) - the full recipe for a new vertical.
- [Unified shell](/architecture/unified-shell) - how the launcher and mounts fit the shell.