Skip to content
Africa OS Knowledgebase
Esc
navigateopen⌘Jpreview
On this page

Application registry

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

  • 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:

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:

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:

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

Last updated on August 18, 2026