Domain · Architecture

Asase — Architecture

Asase is an implemented domain with 15 libraries under libs/asase/ and 5 application packages under apps/asase/.

9sections7 minread

On this page

Food and agriculture operations intelligence domain for Ghana's food value chain. Named after Asase Yaa, the Akan earth goddess of fertility and sustenance.


Asase is the platform layer for Ghana's entire agricultural value chain. It exists because agriculture is uniquely complex in Ghana: the country operates with two distinct rainfall regimes (bimodal in the south, unimodal in the north), local measurement units (olonka, maxi-bag, mini-bag) that differ from international standards, seven active regulatory bodies with overlapping jurisdiction, and a post-harvest spoilage rate of 20–30% on some perishable crops. A generic agriculture framework cannot encode these realities — Asase is built Ghana-first from the ground up.

The domain exposes its capabilities to five consumer applications: an API gateway, a management dashboard, a field mobile application, a digital marketplace, and a processing plant interface. Other Oshun domains reach into Asase through typed connector classes in @asase/connectors, rather than by importing internal libraries directly.


1. Domain Summary#

Asase is an implemented domain with 15 libraries under libs/asase/ and 5 application packages under apps/asase/. It provides the domain layer for Ghana's food and agriculture value chain — covering 19 business units from farm production through processing, cold chain, logistics, quality assurance, export, agricultural inputs, retail/QSR, financial modelling, and market operations.

The domain is designed around the specific reality of Ghana's agricultural sector: dual rainy seasons in the south, a single short season in the north, 16 administrative regions with distinct agro-ecological zones, Ghana-specific units of measure (olonka, maxi-bag, mini-bag), COCOBOD cocoa grading standards, MoFA farm registration, FDA licensing requirements, and GHS as the primary currency.


2. Current Structure#

The workspace is organised as a dependency tree with @asase/core at the root. Specialised libraries build on top of @asase/core for their domain-specific logic, while @asase/infrastructure bridges Asase to shared Oshun platform services (cache, event bus, logging, metrics).

text
libs/asase/
├── core/                   # @asase/core — foundation domain logic
│   └── src/
│       ├── index.ts             # Public API barrel
│       ├── types.ts             # Branded IDs, enums, value objects, entity types
│       ├── constants.ts         # Crop varieties, breeds, regulatory bodies, markets
│       ├── business-units.ts    # Per-unit config interfaces and type guards
│       ├── agro-ecology.ts      # Zone classification and regional profiles
│       ├── measurement.ts       # Unit types, measurement algebra, conversions
│       ├── seasonality.ts       # Crop seasonality profiles and window resolution
│       ├── stakeholders.ts      # Stakeholder types and compliance checks
│       ├── domain-entities.ts   # Crop catalogs, facilities, products, price trends
│       ├── domain-services.ts   # UnitConversion, SeasonalCalendar, Regulatory, Geo, Audit
│       ├── unit-conversion-data.ts  # Shared conversion-factor maps
│       ├── db-schema.ts         # Drizzle ORM tables (36 tables, pgvector support)
│       ├── validation.ts        # Domain-specific validation functions
│       ├── utils.ts             # Agricultural calculation utilities
│       ├── geo-utils.ts         # Haversine distance, DMS, Ghana bounds
│       └── errors.ts            # Domain error hierarchy
├── crops/                  # @asase/crops — registry, variety, pest/disease, field ops, irrigation, soil, yield
├── livestock/              # @asase/livestock — livestock, poultry, hatchery, disease, aquaculture
├── infrastructure/         # @asase/infrastructure — cache, storage, events, metrics, geospatial
├── processing/             # @asase/processing — plant digital twin, scheduling, SPC, OEE
├── cold-chain/             # @asase/cold-chain — temperature, warehouse, GCX receipt, route
├── supply-chain/           # @asase/supply-chain — procurement, distribution, price, demand
├── quality/                # @asase/quality — HACCP, LIMS, recall, certification, calibration
├── export/                 # @asase/export — trade docs, AfCFTA, customs, licensing
├── inputs/                 # @asase/inputs — fertilizer, seed, agrochemical, credit
├── market-intel/           # @asase/market-intel — price intelligence, AfCFTA, policy
├── retail/                 # @asase/retail — QSR, franchise, catering, delivery
├── financials/             # @asase/financials — economics, projection, capex, impact
├── connectors/             # @asase/connectors — cross-domain bridges
├── sota/                   # @asase/sota — satellite, drone, CV, blockchain, ML/RL
├── migrations/             # @asase/migrations — Drizzle migration runner and seeds
├── README.md               # Domain overview and business unit listing
└── tsconfig.json           # Base TypeScript config

Applications under apps/asase/ provide five implemented surfaces: api, dashboard, field, marketplace, and processing.


3. Module Architecture#

@asase/core remains the foundation library, and the domain has expanded into purpose-built packages for crops, livestock, infrastructure, processing, cold chain, supply chain, quality, export, inputs, market intelligence, retail, financials, cross-domain connectors, and SOTA technology, plus the @asase/migrations schema package and five application packages.

Module Dependency Graph (within @asase/core)#

The files inside @asase/core are deliberately layered so that lower-level modules (types, errors, raw data) carry no imports from higher-level ones. This means any file in the graph can be imported in isolation without pulling in the full domain. The graph below shows which modules each file depends on.

text
db-schema.ts
  └── (standalone Drizzle table definitions)

errors.ts
  └── (standalone error hierarchy)

geo-utils.ts
  └── (standalone geographic utilities)

unit-conversion-data.ts
  └── (raw conversion factor maps)

types.ts
  └── (core enums, branded types, value objects)

constants.ts
  └── types.ts

agro-ecology.ts
  └── types.ts

measurement.ts
  └── unit-conversion-data.ts

seasonality.ts
  └── (standalone crop profiles)

stakeholders.ts
  └── types.ts

domain-entities.ts
  └── types.ts, constants.ts

validation.ts
  └── constants.ts

utils.ts
  └── constants.ts, types.ts, agro-ecology.ts

domain-services.ts
  └── types.ts, measurement.ts, agro-ecology.ts,
      seasonality.ts, constants.ts, geo-utils.ts

4. Design Principles#

Ghana-First Domain Model#

The type system is calibrated to Ghana's reality from the ground up rather than being a generic agriculture model with Ghana options bolted on. This means enums, validation patterns, and constants reflect what Ghanaian farmers, regulators, and traders actually encounter:

  • Region enum covers exactly Ghana's 16 administrative regions
  • Season enum uses the names Ghanaian farmers use (major_rainy, minor_rainy, harmattan) not generic seasons
  • MassUnit includes maxi_bag_100kg and mini_bag_50kg; VolumeUnit adds the traditional olonka, bowl, american_tin_small, and american_tin_large measures alongside international units
  • Phone number validation handles MTN, Vodafone, and AirtelTigo Ghana number ranges
  • Farm registration format follows the GH-<region>-<district>-<serial> pattern, validated against the two-letter REGION_CODES

Branded IDs and Structured Value Objects#

Every entity identifier uses a branded string type — Brand<string, '<Name>'> — to prevent ID confusion at compile time. Passing a PlotId where a FarmId is expected becomes a type error, catching a whole class of bugs before runtime. Each branded type comes with a create<Name>Id constructor. Value objects that carry domain semantics are structured interfaces rather than branded numbers, ensuring the units and context travel with the data:

typescript
type FarmId = Brand<string, 'FarmId'>; // build via createFarmId()

interface Temperature {
  celsius: number;
} // temperatureFromCelsius()
interface SoilPH {
  value: number;
  classification: SoilPHClassification;
} // createSoilPH()
interface CurrencyAmount {
  amount: number;
  currency: CurrencyCode;
}
interface PlotArea {
  value: number;
  unit: AreaUnit;
}
interface YieldPerHectare {
  kgPerHectare: number;
}

The measurement.ts module additionally provides phantom-typed numeric measurements (Measurement<U, D>) for compile-time unit safety in mass, volume, area, temperature, humidity, and currency calculations.

Discriminated Union for Business Units#

Asase covers 19 distinct business units — from a bakery to a cold-chain operator to a poultry farm — each with different operational parameters and regulatory obligations. Rather than storing configuration in a generic map or using runtime duck-typing, the 19 business unit configurations use a discriminated union type. This provides exhaustiveness checking in switch statements and precise access to unit-specific fields without runtime casting:

typescript
type BusinessUnitConfig =
  | BakeryConfig // { type: 'bakery'; ovenCount: number; ... }
  | CafeConfig // { type: 'cafe'; seatingCapacity: number; ... }
  | PlantFarmsConfig // { type: 'plant_farms'; totalAreaHectares: number; primaryCrops; ... }
  | PoultryConfig; // { type: 'poultry'; broilerBirdsPerCycle: number; layingHens: number; ... }
// ... all 19

// Type-narrowing via type predicates
function isBakeryConfig(config: BusinessUnitConfig): config is BakeryConfig {
  return config.type === 'bakery';
}

Measurement Algebra#

Agricultural calculations are especially prone to unit confusion: confusing kg/ha with t/ha can turn a profitable crop projection into a loss. The measurement.ts module implements a phantom-typed measurement algebra over the Measurement<U, D> brand. Conversion functions take the source value plus explicit from/to units and route through a canonical SI base, so there is one correct conversion path and no ambiguity:

typescript
// Construct measurements
const m: Mass = mass(50, MassUnit.KILOGRAMS);
const a: Area = area(2.5, AreaUnit.HECTARES);

// Convert via canonical base unit
const tonnes = convertMass(m, MassUnit.KILOGRAMS, MassUnit.TONNES);
const acres = convertArea(a, AreaUnit.HECTARES, AreaUnit.ACRES);

// Format a numeric value + unit for display
formatQuantity(50, MassUnit.KILOGRAMS); // → "50.00 kg"

Service Class Architecture#

Domain services are implemented as classes rather than functions to support dependency injection and future testability. All services can be instantiated with different data sources:

typescript
// All services can be instantiated with different data sources
const regulatory = new GhanaRegulatoryService(); // uses built-in constants
const geo = new GeolocationService(); // uses built-in region data
const audit = new AuditTrailService(customAdapter); // injectable storage

5. Database Schema Design#

The Drizzle ORM schema in db-schema.ts uses the asase_ prefix throughout — 27 pgEnum types and 36 tables. Schema migrations and seed data live in the separate @asase/migrations package (11 migration files, 8 seed scripts, RLS policy migration, connection-pool presets).

The schema makes three notable design choices that affect how the data layer behaves at runtime:

  • pgvector integration: Crop similarity embeddings stored as 1536-dimension vectors for nearest-neighbour variety recommendations. The vector custom type bridges Drizzle's type system with the pgvector PostgreSQL extension. This enables semantic crop searches (e.g., "find varieties similar to Obatanpa that suit Upper East Region") without full-table scans.

  • Enum types in PostgreSQL: Strongly typed fields use pgEnum for status columns (asase_business_unit, asase_growth_stage, asase_quality_grade, asase_shipment_status) rather than plain strings. This moves enum enforcement into the database layer, preventing invalid status values from being stored even if application-level validation is bypassed.

  • JSONB for flexible metadata: Business unit configuration and product attributes stored as JSONB to accommodate per-unit schema variation without requiring separate tables for each of the 19 configuration types.


6. Technology Stack#

The table below shows the technology choice for each major concern. Drizzle ORM was chosen over Prisma primarily for its explicit, code-first schema definition (no separate schema file) and its first-class support for raw SQL when needed.

Component Technology
Language TypeScript (ESM)
ORM Drizzle ORM
Database PostgreSQL (with pgvector extension)
Validation Zod (consumed internally in validation.ts)
Build @nx/js:tsc
Testing Vitest

7. Project Configuration#

  • Project tags: ["scope:asase", "layer:domain", "type:lib"]
  • Module format: ESM ("type": "module")
  • Build executor: @nx/js:tsc

Build Commands#

If the Nx orchestrator has worktree conflicts, bypass it and run the tools directly from the library directory.

bash
# Test
pnpm nx test @asase/core

# Build
pnpm nx build @asase/core

# Lint
pnpm nx lint @asase/core

# Type check (if Nx has worktree conflicts)
cd libs/asase/core && npx tsc --noEmit

8. Library Footprint#

The domain has expanded from its initial @asase/core foundation to 15 purpose-built libraries plus a migration package, and 5 application packages.

The dependency structure is intentionally flat to keep build times short and avoid circular dependencies. @asase/core is the dependency root: most libraries declare @asase/core as a workspace:* dependency. Three libraries are standalone with no @asase/core dependency — @asase/financials, @asase/connectors, and @asase/sota — because their concerns are sufficiently distinct that they benefit from clean isolation. @asase/infrastructure additionally depends on the shared @oshun/cache, @oshun/event-bus, @oshun/logging, and @oshun/metrics packages. @asase/migrations depends on @asase/core, @oshun/database, and @oshun/logging.

ML and remote-sensing capabilities live in @asase/sota (satellite, drone, computer-vision grading, blockchain traceability, ML yield prediction, RL crop planning). The capabilities once sketched as a "community" library (farmer profiles, cooperatives, extension) are covered by stakeholder types in @asase/core, outgrower/contract-farming in @asase/supply-chain, and farmer registration / advisory in the @asase/field application.


Cross-domain integration is implemented in @asase/connectors, which provides classes and builder functions bridging Asase to six Oshun domains. The connector boundary exists so that Asase does not directly import other domain libraries (which would create a circular dependency or force Asase to depend on domain logic it does not own). Instead, each connector owns a typed payload interface that translates Asase data structures into the vocabulary the receiving domain expects. The six boundaries and the data that crosses each are:

Domain Why the boundary exists and what crosses it
Brigid Brigid owns engineering and physical infrastructure design. Asase sends requirements (irrigation system specs, processing-facility layout constraints, cold-chain siting needs, rural road load ratings); Brigid returns engineering proposals.
Cybele Cybele owns earth and environmental intelligence. Asase sends crop location and land-use data; Cybele returns climate-adaptation recommendations, soil health assessments, water-resource analysis, and biodiversity impact scores.
Freya Freya owns commerce and payment flows. Asase sends marketplace product listings, payment reconciliation requests, and supply-chain financing needs (warehouse receipts, purchase orders); Freya returns transaction results and settlement records.
Saraswati Saraswati owns knowledge and education content. Asase sends structured crop and farming practice data; Saraswati returns farmer training materials and agricultural research outputs formatted for advisory delivery.
Maat Maat owns governance and compliance. Asase sends food-safety compliance reports and land-tenure documentation packages; Maat manages the regulatory lifecycle, audit responses, and dispute records.
Aje Aje owns financial intelligence and credit. Asase sends farmer profiles and commodity data; Aje returns agricultural lending scorecards and commodity hedging structures appropriate to Ghana's forward markets.