---
title: Retail reference
description: A complete walkthrough of the retail application - the reference implementation every other vertical should mirror.
---

`@africaos/retail` is the reference implementation. It is the only application module with a fully built domain feature, and it establishes the patterns every vertical should follow. If you are building an application or a feature, read this package and mirror it.

This page walks the products feature end to end, following one slice of data from the database to the screen and back.

### The feature at a glance

```mermaid
flowchart LR
    DB[("retail.products table")]
    Service["ProductService - Effect program"]
    Functions["products.functions.ts - server functions"]
    Hooks["useProducts - TanStack Query"]
    Components["ProductsPage / ProductDetailPage"]

    DB --> Service
    Service --> Functions
    Functions --> Hooks
    Hooks --> Components
    Components -->|mutations| Hooks
    Hooks --> Functions
    Functions --> Service
    Service --> DB
```

### The domain model

`features/products/schemas/product.ts` defines the domain types with Zod:

```ts
export type ProductStatus = "draft" | "active" | "archived";

export interface Product {
  id: string;
  organizationId: string;
  name: string;
  description: string | null;
  priceCents: number;   // integer cents - never floats for money
  currency: string;
  status: ProductStatus;
  createdAt: Date;
  updatedAt: Date;
}
```

The service returns this domain shape with real `Date`s. The wire shape (`PublicProduct`) serializes dates to ISO strings before crossing the network. Prices are stored as integer cents, the standard money-handling rule.

### The service - where business logic lives

`features/products/service.ts` defines `ProductService`:

| Operation | Permission | Behavior |
| --- | --- | --- |
| `list(organizationId)` | `retail.products.read` | Active first, then drafts, newest first. Archived excluded. |
| `create(organizationId, input)` | `retail.products.create` | Validates with `createProductSchema`, inserts. |
| `update(organizationId, id, patch)` | `retail.products.update` | COALESCE patch onto existing row. |
| `archive(organizationId, id)` | `retail.products.archive` | Sets status `archived`. |

Every operation follows the same three steps:

1. **Guard** - `requirePermission("retail.products.<action>")` before acting.
2. **Scope** - `orgWhere(organizationId)` on the `WHERE` clause, so a product from another organization is unreachable (and fails with `ProductNotFound`).
3. **Type** - rows are parsed into `Product`; `status` goes through `productStatusSchema`.

Reads share one column list spliced in with `sql.literal`, so every query selects the same shape.

### The server functions

`features/products/services/products.functions.ts` exposes:

- `listProductsFn` (GET) - returns `PublicProduct[]`.
- `createProductFn` (POST) - returns `{ product } | { error }`.
- `updateProductFn` (POST) - returns `{ product } | { error }`.
- `archiveProductFn` (POST) - returns `{ product } | { error }`.

The `retail` application slug is a constant (`RETAIL_APPLICATION_SLUG`) passed into the request scope, so the runtime verifies application availability on every call. Error folding maps typed failures to stable codes:

```ts
export type ProductError =
  | "not_found" | "invalid" | "forbidden" | "no_organization" | "unauthenticated" | "unknown";
```

`listProductsFn` degrades any failure - including missing organization and missing capability - to an empty catalog, so the page shows an empty state rather than crashing.

### The hooks

`features/products/hooks/useProducts.ts` is the client-facing surface:

```ts
const { products, isLoading, createProduct, updateProduct, archiveProduct } = useProducts({
  organizationSlug,
});
```

- The query key includes the slug: `["retail", "products", organizationSlug ?? "active"]`, so switching organizations refetches the right catalog.
- Mutations invalidate the list on success, keeping the table in sync.
- `products` defaults to `[]` while loading.

### The pages

- `ProductsPage` - the catalog table/list with a create form.
- `ProductDetailPage` - a single product, matched from `relativePath.match(/^\/products\/([^/]+)$/)`.

Both receive `basePath` and `organizationSlug` and render through the retail shell.

### The shell

`src/components/RetailShell.tsx` and `RetailSidebar.tsx` provide the application's internal chrome. In **embedded mode** (`embedded={true}`) the shell renders content only, because the web app already supplies the sidebar. The `dashboardPath` prop points the back link at `/:organization`.

### The test

`features/products/service.test.ts` exercises the service against `Database.Test` (in-memory PGlite) with a fresh memo map per test, so each test gets an isolated database. The service is exercised without Postgres. See [Testing](/tooling/testing) for the pattern.

### What to copy

When building a new vertical or feature, copy the products feature folder and rename it. The pattern - schema, errors, Effect service with guards and scoping, thin server functions, query hooks, pages - is the platform's standard, and deviating from it costs you the platform's guarantees (tenancy, permissions, and consistent error handling).

### Next steps

- [Anatomy of an application](/applications/anatomy-of-an-app) - the package structure this implements.
- [Building an application](/applications/building-an-application) - the recipe for a new vertical.