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).
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.
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:
Regionenum covers exactly Ghana's 16 administrative regionsSeasonenum uses the names Ghanaian farmers use (major_rainy,minor_rainy,harmattan) not generic seasonsMassUnitincludesmaxi_bag_100kgandmini_bag_50kg;VolumeUnitadds the traditionalolonka,bowl,american_tin_small, andamerican_tin_largemeasures 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-letterREGION_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:
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:
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:
// 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:
// 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
vectorcustom type bridges Drizzle's type system with thepgvectorPostgreSQL 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
pgEnumfor 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.
# 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.
9. Related Domains#
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. |