# Cybele Domain — Technical Specifications

> Status: **IMPLEMENTED**. This specification is grounded in the source under
> `libs/cybele/*`, `apps/cybele/*`, and `libs/contracts/cybele`. Every schema,
> enum, table, endpoint, event, and state machine documented below traces to
> real code in those packages; the source file is named at the head of each
> section. Cybele is the Oshun bounded context for built-environment
> intelligence — land, property, construction, design/BIM, prefab, building
> materials, real-estate finance, industrial parks, hospitality property, civil
> infrastructure, and PropTech — scoped to Ghana's built environment.

This document is the authoritative technical reference for the Cybele domain. It
describes every package, type, database table, API endpoint, domain event, and
invariant in the implemented codebase. Engineers working on Cybele, or on any
domain that integrates with it, should use this document to understand exactly
what data Cybele stores, what operations it exposes, and what contracts it
publishes.

The specification is organized from inside out: §1 inventories all packages and
services; §2 covers the core type system that everything else builds on; §3
describes the PostgreSQL/PostGIS persistence schema; §4 covers the full API
surface (REST, GraphQL, gRPC, WebSocket); §5 documents the stable inter-domain
contracts; §6 lists all domain events and their Kafka topics; §7 explains the
construction on-chain ledger; §8 describes cross-domain integration bridges; §9
lists all environment variables; §10 and §11 state the hard invariants and
verification requirements.

## 1. Package Inventory

### 1.1 Library packages (`libs/cybele/*`)

Cybele's logic is split into 19 focused library packages rather than one
monolithic module. Each package has its own `package.json`, `project.json`, and
`vitest.config.ts` and is independently testable. The table below shows the
directory name, the NPM package name used in imports, and the primary
responsibility of each package.

| Package            | Name                       | Responsibility                                                                           |
| ------------------ | -------------------------- | ---------------------------------------------------------------------------------------- |
| `core`             | `@cybele/core`             | Domain types, branded IDs, enums, Zod validators, type guards, serializers               |
| `common`           | `@cybele/common`           | Shared math, date, geo, and ID helpers                                                   |
| `db`               | `@cybele/db`               | Drizzle/PostgreSQL schema, connection pooling, Redis config, seed data                   |
| `api`              | `@cybele/api`              | Hono API gateway, auth/RBAC, rate limiting, Kafka, GraphQL, gRPC, WebSocket, REST routes |
| `site-analysis`    | `@cybele/site-analysis`    | GIS, demographics, infrastructure access, hazard risk, land registry, site selection     |
| `design`           | `@cybele/design`           | Structural, MEP, sustainability, BIM, drawings, generative design intelligence           |
| `construction`     | `@cybele/construction`     | Scheduling, cost, quality, progress, resource, procurement                               |
| `property`         | `@cybele/property`         | Lease, tenant, maintenance, valuation, analytics, IoT                                    |
| `prefab`           | `@cybele/prefab`           | Modular module catalogs, production, affordability, logistics, assembly                  |
| `materials`        | `@cybele/materials`        | Concrete, paint, steel, tiles, furniture, cross-material logic                           |
| `finance`          | `@cybele/finance`          | Mortgage, REIT, pro forma, crowdfunding, alternative finance                             |
| `financials`       | `@cybele/financials`       | Cross-business-unit pro forma models, cost models, synergies                             |
| `market-intel`     | `@cybele/market-intel`     | Pricing indices, supply pipeline, economic indicators                                    |
| `industrial-parks` | `@cybele/industrial-parks` | Park planning, operations, warehouse inventory                                           |
| `hospitality`      | `@cybele/hospitality`      | Revenue management, operations                                                           |
| `infrastructure`   | `@cybele/infrastructure`   | Roads, civil works, infrastructure procurement                                           |
| `proptech`         | `@cybele/proptech`         | Listings, CRM, digital experience, transactions, blockchain title                        |
| `integration`      | `@cybele/integration`      | Cross-domain bridges to Brigid, Saraswati, Asase, Freya, Maat                            |
| `testing`          | `@cybele/testing`          | Fixtures and mocks                                                                       |

### 1.2 Contracts package (`libs/contracts/cybele`)

`@contracts/cybele` — stable cross-domain schemas. Source: `src/api-schemas.ts`
(request/response Zod schemas), `src/events.ts` (CloudEvents envelope, event
type registry, Kafka topic registry, event payload types). Re-exported from
`src/index.ts`.

### 1.3 Application services (`apps/cybele/*`)

Five independently deployable services compose the domain. Each service is
responsible for one operational workflow and references the library packages
above for its domain logic. The table shows the app directory, the package name,
and the HTTP route surface each app owns.

| App                       | Name                      | Surface                                                                                                                                                        |
| ------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/cybele/api`         | `@cybele/app-api`         | Cybele API gateway: routing, JWT auth, rate limiting, versioning, audit logging, OpenAPI doc endpoint, webhook management (`src/routes/gateway.ts`)            |
| `apps/cybele/projects`    | `@cybele/app-projects`    | Construction project management: dashboard, Gantt schedule, daily reports, RFIs, change orders, QC, safety, earned value, documents (`src/routes/projects.ts`) |
| `apps/cybele/portfolio`   | `@cybele/app-portfolio`   | Property portfolio: summary, property detail, tenants, rent collection, maintenance, financials, valuation, lease calendar (`src/routes/portfolio.ts`)         |
| `apps/cybele/marketplace` | `@cybele/app-marketplace` | PropTech marketplace: listings, search, recommendations, agents, inquiries/leads, viewings, comparison, analytics, favourites (`src/routes/marketplace.ts`)    |
| `apps/cybele/factory`     | `@cybele/app-factory`     | Prefab factory: production orders, station scheduling, QC, material inventory, logistics, KPI dashboard, module configuration (`src/routes/factory.ts`)        |

### 1.4 Workspace facts

- Nx scope tag: `scope:cybele` (from each `project.json`).
- Cybele API port: `4027`; database schema name: `cybele` (from
  `libs/cybele/README.md`).
- Phase reference for the build-out: TODO Phase 58 (`TODOS/phase-58.md`); the
  Gaia weather/climate integration is Phase 175. The `db` schema sections carry
  inline phase subtask anchors such as `58.1.2.6`, `58.1.2.9`, `58.1.2.11`,
  `58.1.2.12`; the API modules carry `58.1.3.x` anchors.

---

## 2. Core Built-Environment Type System

Source: `libs/cybele/core/src/types.ts`, with index re-exports in
`libs/cybele/core/src/index.ts` (`types`, `validators`, `guards`,
`serializers`). The core library is the canonical type system; all monetary
values are in Ghana Cedis (GHS) unless a field explicitly carries a currency.

This section documents every type defined in `@cybele/core`. Subsections 2.1–2.4
cover the foundational types (IDs, enums, spatial, valuation) that everything
else builds on. Subsections 2.5–2.13 cover the primary domain objects (plots,
properties, construction, contractors, leases, finance, materials, facilities).
Subsections 2.14–2.18 cover supplementary types for construction details, tenant
management, investment instruments, manufacturing, and the Ghana Lands
Commission API integration.

### 2.1 Branded identifier types

Cybele uses branded string types for each entity's primary key. This gives
compile-time identity safety — you cannot accidentally pass a `TenantId` where a
`ProjectId` is expected — without any runtime overhead.

`types.ts` defines twelve branded string ID types, each with a `make…` factory
function (`makePropertyId`, `makePlotId`, etc.):

`PropertyId`, `PlotId`, `BuildingId`, `UnitId`, `TitleId`, `ProjectId`,
`TenantId`, `LeaseId`, `ContractorId`, `MaterialId`, `InvestorId`, `FundId`.

Each is `string & { readonly __brand: '<Name>' }`, giving compile-time identity
safety without runtime cost.

### 2.2 Enumerations

The enumerations below are the state machines and classification vocabularies
for the domain. They are string-valued TypeScript enums, so their values appear
as readable strings in JSON payloads. Each is also wrapped in a Zod
`z.nativeEnum(...)` schema for runtime validation; those schemas are exported
from `validators.ts` using the naming convention `<EnumName>Schema`.

`types.ts` defines the following TypeScript `enum`s (string-valued):

- **`PropertyStatus`** — `Planning`, `UnderConstruction`, `Completed`,
  `ForSale`, `ForRent`, `Occupied`, `UnderRenovation`, `Demolished`.
- **`ConstructionStatus`** — `Bidding`, `Awarded`, `Mobilization`, `InProgress`,
  `Substantial` (substantial completion), `Defects` (defects liability period),
  `FinalCompletion`.
- **`LeaseType`** — `FixedTerm`, `MonthToMonth`, `RentToOwn`, `GroundLease`,
  `TripleNet`, `GrossLease`.
- **`PropertyType`** — `Residential`, `Commercial`, `Industrial`, `MixedUse`,
  `Land`, `Hospitality`, `Healthcare`, `Education`.
- **`ValuationMethod`** — `Comparative`, `Income`, `Cost`, `DCF`.
- **`ProjectPhaseType`** — `Preconstruction`, `Foundation`, `Structure`, `MEP`
  (mechanical/electrical/plumbing), `Finishing`, `Handover`.
- **`GhanaRegion`** — all 16 administrative regions of Ghana plus the legacy
  `Brong_Ahafo`: `Greater_Accra`, `Ashanti`, `Western`, `Central`, `Eastern`,
  `Northern`, `Upper_East`, `Upper_West`, `Volta`, `Brong_Ahafo`, `Oti`, `Bono`,
  `Bono_East`, `Ahafo`, `Western_North`, `North_East`, `Savannah` (17 values).
- **`ZoningClass`** — `Residential_Low`, `Residential_Medium`,
  `Residential_High`, `Commercial`, `Industrial_Light`, `Industrial_Heavy`,
  `Mixed_Use`, `Agricultural`, `Conservation`, `Institutional`.
- **`LandTenure`** — `Freehold`, `Leasehold`, `Stool_Land`, `Family_Land`,
  `Vested` — the customary and statutory tenure categories of Ghanaian land law.

Zod schemas wrapping each enum via `z.nativeEnum(...)` are exported from
`validators.ts` (`PropertyStatusSchema`, `ConstructionStatusSchema`,
`LeaseTypeSchema`, `PropertyTypeSchema`, `ValuationMethodSchema`,
`ProjectPhaseTypeSchema`, `GhanaRegionSchema`, `ZoningClassSchema`,
`LandTenureSchema`).

### 2.3 Spatial and address types

All property and listing locations are expressed in Ghana-specific address and
geospatial types. The address type enforces Ghana Post GPS digital-address
formatting; the geospatial types always carry an explicit spatial reference ID
(`srid`) so coordinates are never stored without their coordinate system.

- **`GhanaAddress`** — `region` (`GhanaRegion`), `district`, `locality`,
  `streetName`, `houseNumber`, `digitalAddressGps`, `landmark`, `postalCode`.
  `digitalAddressGps` holds a Ghana Post GPS digital address; the canonical
  format is two uppercase letters, hyphen, three digits, hyphen, four digits
  (e.g. `GE-123-4567`), enforced by the regex `^[A-Z]{2}-\d{3}-\d{4}$` in
  `GhanaAddressSchema`.
- **`GeoLocation`** — `latitude`, `longitude`, `elevation` (metres above sea
  level), `srid` (Spatial Reference ID; default WGS-84 = `4326`).
- **`GeoPolygon`** — `coordinates` (ordered ring of `[longitude, latitude]`
  pairs), `srid`, `area` (m²), `perimeter` (m).

`GeoLocationSchema` constrains `latitude` to `[-90, 90]`, `longitude` to
`[-180, 180]`, and defaults `srid` to `4326`.

### 2.4 Valuation and zoning

Valuation records capture both the computed value and the methodology and
assumptions behind it, enabling auditable reproductions of any past valuation.
Zoning classifications carry the full set of planning constraints that site
analysis and design checks evaluate against.

- **`PropertyValuation`** — `id`, `propertyId`, `method` (`ValuationMethod`),
  `value` (GHS), `currency`, `date`, `valuer` (registered valuer/firm),
  `confidence` (0–1 coefficient), `assumptions` (string list),
  `marketComparables` (optional `MarketComparable[]`).
- **`MarketComparable`** — `address`, `saleDate`, `salePrice`, `area`,
  `pricePerSqm`, `adjustmentFactor`, `adjustedPrice`.
- **`ZoningClassification`** — `class` (`ZoningClass`), `permittedUses`,
  `maxDensity` (dwelling units/hectare), `maxHeight` (m), `setbackFront`,
  `setbackSide`, `setbackRear` (m), `farRatio` (Floor Area Ratio).

### 2.5 Land title

A `LandTitle` is the formal document that establishes ownership of a plot under
Ghanaian land law. Its `expiryDate` is only populated for leasehold titles;
freehold and stool-land titles do not expire. The `isDisputed` flag triggers
additional due-diligence checks in site analysis.

**`LandTitle`** — `id` (`TitleId`), `titleNumber`, `registrationDate`,
`ownerId`, `ownerName`, `plotId` (`PlotId`), `encumbrances` (string list),
`area` (m² per survey plan), `surveyPlanRef`, `issueAuthority`, `expiryDate`
(`Date | null` — only applicable to leasehold titles), `isDisputed`.

### 2.6 Property and property subtypes

`Property` is the central domain object — the unit of valuation, portfolio
management, and PropTech listing. A base `Property` record holds fields common
to all property types. Four discriminated subtypes extend it, each adding fields
relevant to its specific use class. The `type` literal on each subtype allows
TypeScript to narrow to the correct shape. Duck-typed type guards in `guards.ts`
test for subtype by field presence rather than `instanceof`, so they work across
serialization boundaries.

**`Property`** (base) — `id`, `type` (`PropertyType`), `status`
(`PropertyStatus`), `address` (`GhanaAddress`), `location` (`GeoLocation`),
`grossArea` (m²), `netArea` (net usable, m²), `currentValue` (GHS),
`lastValuation` (`PropertyValuation`), `titleId`, `yearBuilt`, `description`,
`amenities`, `images`, `createdAt`, `updatedAt`.

Four discriminated subtypes extend `Property`, each pinned by its `type`
literal:

- **`ResidentialProperty`** (`type: Residential`) — `bedrooms`, `bathrooms`,
  `floorArea`, `hasGarden`, `parkingSpaces`, `hasPool`, `furnishing`
  (`furnished` | `semi-furnished` | `unfurnished`), `targetMarket` (`low-income`
  | `middle-income` | `high-income` | `luxury`).
- **`CommercialProperty`** (`type: Commercial`) — `officeGrade` (`A`|`B`|`C`),
  `floorPlates`, `columnSpacing`, `ceilingHeight`, `loadingDocks`,
  `generatorCapacity` (kVA), `fibreConnected`.
- **`IndustrialProperty`** (`type: Industrial`) — `warehouseArea`, `officeArea`,
  `loadingDocks`, `clearHeight` (m), `craneCapacity` (tonnes; 0 if no crane),
  `yardSpace`, `powerCapacity` (kVA), `hasRailAccess`, `hasColdStorage`.
- **`MixedUseProperty`** (`type: MixedUse`) — `retailPct`, `officePct`,
  `residentialPct`, `hospitalityPct` (each 0–100), `totalUnits`, `retailGLA`
  (gross leasable area for retail, m²), `anchorTenant`.

`guards.ts` provides duck-typed (field-presence, not `instanceof`) type guards
for each property subtype and for the specialized facility types.

### 2.7 Plot, building, unit

These three types form the physical hierarchy of a developed property: a `Plot`
is a parcel of land, a `Building` sits on a plot, and `Unit`s subdivide a
building into individually leasable or saleable spaces. Site analysis works at
the `Plot` level; construction at the `Building` level; leasing at the `Unit`
level.

- **`Plot`** — `id` (`PlotId`), `titleId`, `area` (m²), `boundary`
  (`GeoPolygon`), `zoning` (`ZoningClassification`), `topography` (`flat` |
  `gentle_slope` | `steep_slope` | `undulating`), `soilType`, `encumbrances`,
  `isServiced` (roads + power + water + sewer present), `tenure` (`LandTenure`),
  `price` (GHS), `pricePerSqm` (GHS).
- **`Building`** — `id` (`BuildingId`), `propertyId`, `name`, `floors`,
  `totalUnits`, `grossArea`, `netLeasableArea`, `structuralSystem` (`rc_frame` |
  `steel_frame` | `load_bearing` | `timber`), `yearBuilt`, `condition`
  (`excellent` | `good` | `fair` | `poor`), `hasLift`, `hasBackupPower`,
  `hasBorehole`.
- **`Unit`** — `id` (`UnitId`), `buildingId`, `unitNumber`, `floor`, `area`,
  `type` (`studio` | `1br` | `2br` | `3br` | `4br` | `penthouse` | `office` |
  `retail` | `warehouse`), `status` (`available` | `occupied` | `maintenance` |
  `reserved`), `currentRent` (contracted GHS/month), `marketRent` (estimated
  GHS/month), `tenantId` (`TenantId | null`).

### 2.8 Construction project, phase, milestone

A `ConstructionProject` is broken into ordered `ProjectPhase` records, each
containing `Milestone` records. This three-level hierarchy (project → phase →
milestone) mirrors the standard PMI/FIDIC project breakdown. Milestones that
carry a `linkedPayment` value are payment milestones — when `milestone_achieved`
is recorded on the construction ledger, the linked amount is unlocked for
disbursement.

- **`Milestone`** — `id`, `name`, `plannedDate`, `actualDate` (`Date | null`),
  `isPredecessorComplete`, `completionCriteria`, `isCriticalPath`,
  `linkedPayment` (GHS unlocked by this milestone, or `null` for a non-payment
  milestone).
- **`ProjectPhase`** — `id`, `name` (`ProjectPhaseType`), `status`
  (`ConstructionStatus`), `plannedStart`, `plannedEnd`, `actualStart`,
  `actualEnd` (nullable), `percentComplete`, `cost` (GHS), `milestones`
  (`Milestone[]`).
- **`ConstructionProject`** — `id` (`ProjectId`), `name`, `type` (`residential`
  | `commercial` | `industrial` | `infrastructure` | `mixed_use`), `status`
  (`ConstructionStatus`), `clientId`, `propertyId`, `location` (`GhanaAddress`),
  `contractValue` (original, GHS), `revisedContractValue` (including variations,
  GHS), `startDate`, `plannedCompletionDate`, `forecastCompletionDate`,
  `percentComplete` (0–100), `projectManager`, `mainContractorId`
  (`ContractorId`), `consultants` (architect/engineer/QS names), `phases`
  (`ProjectPhase[]`).

### 2.9 Contractor and subcontractor

`Contractor` records represent main contractors; `Subcontractor` extends
`Contractor` with trade-specific fields. The `registrationNumber` is the Ghana
Revenue Authority or business registration number — a compliance requirement for
public procurement in Ghana. The `bondingCapacity` sets the maximum contract
value a contractor can be awarded.

- **`Contractor`** — `id` (`ContractorId`), `companyName`, `registrationNumber`
  (Ghana Revenue Authority or business registration number), `licenseType`,
  `licenseExpiry`, `specializations`, `rating` (1–5, 5 = excellent),
  `bondingCapacity` (max GHS), `completedProjects`, `ongoingProjects`,
  `contactEmail`, `contactPhone`.
- **`Subcontractor`** extends `Contractor` — adds `tradeSpecialisation`,
  `crewSize`, `parentContractorId` (`ContractorId`, optional),
  `dailyCrewRateGhs`, `ownedEquipment` (string list).

### 2.10 Tenant, lease, rent schedule

The tenant and lease types model the full rental relationship. A `Tenant`
represents a person or company; a `Lease` binds a tenant to a specific unit for
a period; and `RentSchedule` is the period-by-period ledger generated from the
lease terms. The `escalationType` field determines how rent increases at each
review: a fixed percentage, CPI-linked (with a 4.5% CPI floor applied in the
API), or open-market review.

- **`Tenant`** — `id` (`TenantId`), `name`, `type` (`individual` | `corporate`),
  `contactEmail`, `contactPhone`, `idNumber` (national ID, passport, or company
  registration number), `creditScore` (300–850), `employerName`, `monthlyIncome`
  (verified GHS), `rentalHistory` (`good` | `fair` | `poor`), `createdAt`.
- **`Lease`** — `id` (`LeaseId`), `unitId`, `tenantId`, `type` (`LeaseType`),
  `startDate`, `endDate`, `rentAmount` (GHS/month), `rentCurrency` (`GHS` |
  `USD`), `escalationType` (`fixed_pct` | `cpi_linked` | `market_review`),
  `escalationRate` (annual %), `depositAmount`, `depositHeld`, `terms` (string
  list), `isActive`, `renewalOption`, `breakOption`.
- **`RentSchedule`** — `leaseId`, `periodStart`, `periodEnd`, `dueDate`,
  `amount` (billed GHS), `paid` (received GHS), `balance` (`amount − paid`),
  `status` (`current` | `overdue` | `paid` | `partial`), `daysOverdue`,
  `lateFee`.

### 2.11 Finance primitives

The finance primitives model the instruments used to finance property
acquisition and development. `MortgageProduct` defines the terms of a loan
product; `AmortizationEntry` is one row in a repayment schedule; `REITFund` is a
real-estate investment trust or fund; and `DevelopmentProForma` is the
project-level feasibility model showing cost, revenue, IRR, and equity multiple.

- **`MortgageProduct`** — `id`, `name`, `rateType` (`fixed` | `variable` |
  `hybrid`), `nominalRate`, `effectiveRate` (annual %, fees/compounding
  included), `maxLTV` (max loan-to-value %), `minTenureMonths`,
  `maxTenureMonths`, `minLoanAmount`, `maxLoanAmount`, `eligibilityCriteria`.
- **`AmortizationEntry`** — `period`, `paymentDate`, `openingBalance`,
  `payment`, `principal`, `interest`, `closingBalance`.
- **`REITFund`** — `id` (`FundId`), `name`, `ticker`, `nav` (GHS), `navPerUnit`,
  `unitsOutstanding`, `distributionYield` (%), `totalAssets`,
  `totalLiabilities`, `sectorAllocation` (`Record<string, number>`, values must
  sum to 100), `inceptionDate`.
- **`DevelopmentProForma`** — `projectId`, `landCost`, `hardCosts`, `softCosts`,
  `contingency`, `financingCosts`, `totalCost`, `grossRevenue`, `netRevenue`,
  `profit`, `profitMargin` (% of total cost), `irr` (%), `equityMultiple` (net
  revenue / equity invested), `paybackPeriod`.

### 2.12 Manufacturing and materials primitives

These types represent the physical products manufactured or sourced by Cybele's
factory and materials operations. `PrefabModule` is the base unit of modular
housing. The concrete, paint, steel, and tile product types carry Ghana
Standards Authority certification fields (`gsCertified`, `ghanaStandard`) that
are required for construction compliance in Ghana.

- **`PrefabModule`** — `id`, `designCode`, `type` (`studio` | `1br` | `2br` |
  `3br` | `classroom` | `clinic` | `office`), `widthM`, `lengthM`, `heightM`,
  `weightKg`, `structuralCapacity` (kN/m² floor load), `connections` (array of
  `end` | `side` | `top` | `bottom`), `productionStatus` (`design` |
  `fabrication` | `qc` | `storage` | `transit` | `installed`), `unitCost`.
- **`ConcreteBlock`** — `dimensions` (`150x225x450mm` | `200x225x450mm` |
  `100x225x450mm`), `strengthNmm2` (compressive, N/mm²), `density` (kg/m³),
  `hasHollow`, `gsCertified` (Ghana Standards Authority), `batchNumber`,
  `testDate`, `testResult` (`pass` | `fail`).
- **`PaintProduct`** — `id`, `formulationCode`, `name`, `color`, `hexCode`,
  `finish` (`matte` | `eggshell` | `satin` | `semi_gloss` | `gloss`), `vocGPerL`
  (volatile organic compounds, g/L), `coverageM2PerL`, `dryingTimeHours`,
  `shelfLifeMonths`.
- **`SteelProduct`** — `id`, `profile` (`I_beam` | `H_beam` | `channel` |
  `angle` | `flat_bar` | `round_bar` | `hollow_section`), `gradeS` (EN 10025:
  `S275` | `S355` | `S420`), `lengthM`, `weightKgPerM`, `cross_section_area`
  (cm²), `millCertificate`.
- **`BuildingMaterial`** — `id` (`MaterialId`), `name`, `specificationCode`,
  `ghanaStandard` (optional, Ghana Standards Authority code), `unit`,
  `unitCostGhs`, `supplierName`, `supplierContact`, `leadTimeDays`,
  `minimumOrderQty`, `certificationBody` (optional), `certificationNumber`
  (optional), `isPrequalified`, `embodiedCarbonKgCo2` (optional, kg CO₂e per
  unit), `isGreenCertified`.

### 2.13 Specialized facility types

Beyond the four property subtypes, Cybele tracks additional managed-asset types
for specialized built-environment contexts. Each has a `propertyId` back-
reference where applicable, linking it to the base `Property` record for
portfolio management and valuation. Type guards for these are defined in
`guards.ts`.

`types.ts` defines additional managed-asset records, each with `propertyId`
back-references where applicable:

- **`IndustrialPark`** — `id`, `name`, `location`, `totalArea` (hectares),
  `developedArea`, `plots`, `vacantPlots`, `occupancyRate` (%), `isFreeZone`
  (GIPC Free Zone status), `anchorTenants`, `utilities` (array of `power` |
  `water` | `sewer` | `fibre` | `gas`), `masterPlanUrl`.
- **`DataCenter`** — `id`, `name`, `tierLevel` (Uptime Institute Tier 1–4),
  `powerCapacityMW`, `pueTarget` (Power Usage Effectiveness), `rackCount`,
  `totalFloorArea`, `colocationAvailable` (m²), `connectivity`,
  `certifications`.
- **`HospitalityProperty`** — `id`, `propertyId`, `name`, `starRating` (1–5),
  `roomCount`, `serviceType` (`hotel` | `serviced_apartment` | `resort` |
  `guesthouse`), `amenities`, `revpar` (Revenue Per Available Room, GHS), `adr`
  (Average Daily Rate, GHS), `occupancyRate` (%).
- **`StudentHousing`** — `id`, `propertyId`, `university`, `bedCount`,
  `distanceToMainGate` (km), `amenities`, `rentPerBedPerAnnum` (GHS),
  `occupancyRate` (%), `mealsIncluded`.
- **`RetailProperty`** — `id`, `propertyId`, `gla` (gross leasable area, m²),
  `anchorTenant`, `footTrafficPerDay`, `parkingRatio` (spaces per 100 m² GLA),
  `vacancyRate` (%), `averageRentPerSqm` (GHS/m²/annum).
- **`ColdStorageFacility`** — `id`, `propertyId`, `temperatureZones`
  (`TemperatureZone[]`), `hasBlasFreezer`, `backupPowerKW`, `certifications`.
- **`TemperatureZone`** — `name`, `minTemp`, `maxTemp`, `capacityPallets`.

### 2.14 Construction supplementary types

These types provide the detail behind a `ConstructionProject`. A
`ConstructionTask` is the atomic WBS unit tracked for earned-value purposes.
`Blueprint` and `BIMModel` record the drawing and model artefacts for a project.
`ChangeOrder` captures approved scope/cost/time changes with an approval chain.
`QCInspection` records the result of an on-site quality check. `SafetyIncident`
captures OSHA-classified site incidents with follow-up action tracking.
`WeatherCondition` captures daily site weather and its productivity impact —
this is the record Gaia data enriches at Phase 175.

- **`ConstructionTask`** — the atomic WBS unit. Fields: `id`, `projectId`,
  `projectPhaseId`, `wbsCode` (PMI convention, e.g. `1.2.3.4`), `description`,
  `durationDays` (working days), `predecessorIds` (finish-to-start by default),
  `plannedStart`, `plannedFinish`, `actualStart` (optional), `actualFinish`
  (optional), `percentComplete` (0–100), `resources` (array of
  `{ type: labour | plant | material, description, quantity, unit, unitCostGhs }`),
  `plannedValueGhs` (BCWS — budgeted cost of work scheduled), `isCriticalPath`,
  `totalFloat` (slack, working days), `notes` (optional).
- **`ConstructionEquipment`** — `id`, `name`, `category` (`tower_crane` |
  `mobile_crane` | `excavator` | `bulldozer` | `dumper` | `concrete_mixer` |
  `concrete_pump` | `compactor` | `generator` | `scaffolding` | `formwork` |
  `forklift` | `piling_rig` | `other`), `makeModel`, `capacityDescription`,
  `dailyRateGhs`, `weeklyRateGhs`, `operatorIncluded`, `availability`
  (`available` | `on_hire` | `under_maintenance` | `decommissioned`),
  `currentProjectId` (optional), `availableFrom` (optional), `safetyCertExpiry`,
  `ownerName`, `ownerContact`.
- **`Blueprint`** — architectural/structural/MEP drawing. `id`, `projectId`,
  `drawingNumber`, `revision`, `title`, `discipline` (`architectural` |
  `structural` | `civil` | `mechanical` | `electrical` | `plumbing` |
  `fire_protection` | `landscape`), `scale`, `format` (`A0`–`A4`), `fileUrl`,
  `fileSizeBytes`, `fileFormat` (`pdf` | `dwg` | `dxf` | `svg`),
  `issuedForConstructionDate` (optional), `status` (`draft` | `in_review` |
  `approved` | `issued_for_construction` | `superseded` | `void`), `drawnBy`,
  `checkedBy` (optional), `approvedBy` (optional), `createdAt`, `updatedAt`.
- **`BIMModel`** — `id`, `projectId`, `modelId` (unique within the Common Data
  Environment), `lod` (Level of Development 100/200/300/350/400/500),
  `discipline` (`architectural` | `structural` | `mep` | `civil` | `site` |
  `federated`), `softwareVersion`, `fileFormat` (`ifc` | `rvt` | `nwd` | `nwf` |
  `nwc` | `skp` | `rfa`), `ifcSchema` (optional: `IFC2X3` | `IFC4` | `IFC4X3`),
  `fileUrl`, `fileSizeBytes`, `coordinateSystem`, `elementCount` (optional),
  `status` (`draft` | `in_review` | `approved` | `published` | `archived`),
  `authoredBy`, `createdAt`, `updatedAt`.
- **`ChangeOrder`** — `id`, `projectId`, `voNumber`, `submittedDate`,
  `description`, `justification`, `costImpactGhs` (positive = addition, negative
  = omission), `scheduleImpactDays` (positive = extension), `raisedBy` (`client`
  | `contractor` | `consultant` | `statutory_authority`), `approvalChain`
  (ordered array of
  `{ approverName, approverRole, status: pending|approved|rejected, decisionDate?, comments? }`),
  `status` (`draft` | `submitted` | `under_review` | `approved` | `rejected` |
  `implemented`), `approvedCostGhs` (optional), `approvedScheduleDays`
  (optional), `supportingDocs`.
- **`QCInspection`** — `id`, `projectId`, `location`, `category`
  (`concrete_pour` | `rebar_placement` | `formwork` | `waterproofing` |
  `blockwork` | `roofing` | `plastering` | `tiling` | `electrical` | `plumbing`
  | `final_completion`), `inspectionDate`, `inspector`, `inspectorOrganisation`,
  `checklist` (array of
  `{ itemCode, description, result: pass|fail|n_a|observation, remarks? }`),
  `nonConformanceReports` (array of
  `{ ncrNumber, description, severity: minor|major|critical, responsibleParty, closureDate?, closureStatus: open|in_progress|closed| accepted_risk }`),
  `overallResult` (`pass` | `conditional_pass` | `fail` | `pending_retest`),
  `contractorSignoff` (optional), `clientSignoff` (optional), `attachments`.
- **`SafetyIncident`** — `id`, `projectId`, `incidentDateTime`, `severity`
  (`near_miss` | `first_aid` | `medical_treatment` | `lost_time` |
  `permanent_disability` | `fatality`), `oshaClassification` (`recordable` |
  `non_recordable` | `first_aid_only` | `fatality`), `description`, `location`,
  `injuredPartyName` (optional), `injuredPartyRole` (optional),
  `injuredPartyContractor` (optional `ContractorId`), `bodyPartAffected`
  (optional), `injuryType` (optional), `lostWorkDays`, `rootCauses` (array of
  `unsafe_act` | `unsafe_condition` | `management_system_failure` |
  `environmental` | `equipment_failure`), `rootCauseDescription`,
  `immediateActions`, `preventiveActions` (array of
  `{ action, responsiblePerson, dueDate, status: open|in_progress|closed }`),
  `requiresRegulatoryNotification`, `regulatoryNotificationSentDate` (optional),
  `reportedBy`, `investigatedBy` (optional), `status` (`reported` |
  `under_investigation` | `closed` | `regulatory_review`).
- **`WeatherCondition`** — daily site weather affecting productivity. `id`,
  `projectId`, `date`, `temperatureCelsius` (`{ min, max, avg }`),
  `humidityPct`, `windSpeedKmh`, `windDirection`, `precipitationMm`,
  `visibilityKm`, `weatherType` (`clear` | `partly_cloudy` | `overcast` |
  `light_rain` | `heavy_rain` | `storm` | `harmattan` | `fog`),
  `productivityMultiplier` (1.0 = no impact, 0.5 = 50% loss), `workStoppage`,
  `stoppageHours`, `dataSource` (`ghana_met` | `on_site_station` |
  `manual_entry`), `recordedBy` (optional).

### 2.15 Tenant and lease supplementary types

These types extend the core tenant/lease model with operational detail.
`MaintenanceRequest` is the helpdesk record raised against a unit.
`ServiceCharge` is the annual budget-and-allocation reconciliation for
common-area costs shared across tenants. `TenantScreening` is the pre-tenancy
due-diligence record. `Eviction` tracks the legally required
notice-and-proceedings process under the Ghana Rent Act.

- **`MaintenanceRequest`** — `id`, `propertyId`, `unitId` (optional), `tenantId`
  (optional — null = raised by property manager), `category` (`plumbing` |
  `electrical` | `hvac` | `structural` | `appliances` | `pest_control` |
  `cleaning` | `security` | `landscaping` | `other`), `description`, `priority`
  (`emergency` | `high` | `medium` | `low`), `reportedAt`,
  `targetCompletionDate` (SLA target), `completedAt` (optional),
  `assignedVendorId`/`assignedVendorName`/ `assignedTechnicianName` (optional),
  `status` (`open` | `assigned` | `in_progress` | `pending_parts` | `completed`
  | `cancelled` | `on_hold`), `estimatedCostGhs`/`actualCostGhs` (optional),
  `costResponsibility` (`landlord` | `tenant` | `shared`), `satisfactionRating`
  (optional 1–5), `photos`, `notes`.
- **`ServiceCharge`** — annual service-charge budget and per-tenant allocation.
  `id`, `propertyId`, `periodStart`, `periodEnd`, `totalGlaM2`, `budgetItems`
  (array keyed by `category` ∈ {`security`, `cleaning`,
  `electricity_common_areas`, `water_and_sewage`, `waste_management`,
  `landscaping`, `lift_maintenance`, `hvac_maintenance`, `building_insurance`,
  `management_fee`, `reserve_fund`, `other`} with `budgetedGhs` and optional
  `actualGhs`), `totalBudgetGhs`, `totalActualGhs` (optional), `allocationBasis`
  (`floor_area` | `equal_share` | `unit_count`), `tenantAllocations` (per-tenant
  `{ tenantId, unitId, tenantGlaM2, allocationPct, chargeGhs, isInvoiced, invoiceDate?, paidDate? }`),
  `status` (`draft` | `approved` | `invoiced` | `reconciled`).
- **`TenantScreening`** — pre-tenancy assessment. `id`, `prospectiveTenantId`,
  `propertyId`, `unitId` (optional), `initiatedDate`, `completedDate`
  (optional), `creditCheck`
  (`{ provider, score, rating: exceptional|very_good|good|fair|poor, adverseItems, checkedDate }`),
  `employmentVerification`
  (`{ employerName, employerContact, monthlyGrossIncomeGhs, employmentType: permanent|contract|self_employed| retired|unemployed, yearsEmployed, verified, verificationMethod: payslip|bank_statement|employment_letter|tax_return }`),
  `rentalReferences` (array of prior-landlord records), `identityVerification`
  (`{ idType: ghana_card|passport|drivers_license|voters_id, idNumber, verified, verifiedBy? }`),
  `rentToIncomeRatio`, `affordabilityCheck`, `recommendation` (`approve` |
  `approve_with_conditions` | `reject` | `pending`), `conditions` (optional),
  `notes` (optional), `screenedBy`.
- **`Eviction`** — `id`, `leaseId`, `tenantId`, `propertyId`, `unitId`,
  `grounds` (`non_payment_of_rent` | `breach_of_tenancy_terms` |
  `property_damage` | `illegal_activities` | `owner_occupation` |
  `redevelopment` | `expiry_no_renewal`), `noticeServedDate`,
  `requiredNoticeDays` (Ghana Rent Act notice period), `vacatePossessionDate`,
  `rentArrearsGhs`, `status` (`notice_served` | `negotiation_in_progress` |
  `court_proceedings` | `court_order_obtained` | `bailiff_instructed` |
  `property_vacated` | `withdrawn`), `courtProceedings` (optional nested
  record), `settlement` (optional nested record), `actualVacateDate` (optional),
  `legalCostsGhs` (optional), `handledBy`, `notes`.

### 2.16 Finance and investment supplementary types

These types cover the full investment lifecycle beyond the basic finance
primitives. `MortgageApplication` is the underwriting record for a loan
application, including Bank of Ghana DTI compliance. `InvestorAllocation` tracks
a single investor's stake in a REIT or fund. `EquityWaterfall` defines the JV
distribution waterfall tiers. `CapitalCall` is the formal drawdown notice to
investors. `CrowdfundingCampaign` tracks a SEC Ghana–registered property
crowdfunding offering. `DiasporaInvestment` models overseas-resident investment
with remittance channels, AML checks, and Bank of Ghana approval references.

- **`MortgageApplication`** — `id`, `applicantId`, `applicantName`, `propertyId`
  (optional), `productId`, `loanAmountGhs`, `tenureMonths`, `annualRatePct`,
  `grossMonthlyIncomeGhs`, `monthlyDebtObligationsGhs`, `debtToIncomeRatio`
  (Bank of Ghana guideline: ≤ 43%), `creditScore` (optional), `employmentType`
  (`public_sector` | `private_sector` | `self_employed` | `diaspora`),
  `loanToValueRatio`, `supportingDocuments` (array of
  `{ type: payslip|bank_statement|tax_return|employment_letter| business_financials, fileUrl, verifiedAt?, verifiedBy? }`),
  `status` (`submitted` | `document_review` | `credit_assessment` |
  `property_valuation` | `offer_issued` | `offer_accepted` | `legal_processing`
  | `approved` | `rejected` | `withdrawn`), `underwritingNotes` (optional),
  `approvedAmountGhs`/`approvedRatePct` (optional),
  `offerIssuedDate`/`offerExpiryDate`/`decisionDate` (optional),
  `rejectionReasons` (optional), `createdAt`, `updatedAt`.
- **`InvestorAllocation`** — investor stake in a REIT/fund. `id`, `investorId`,
  `fundId`, `unitsAllocated`, `capitalCommittedGhs`, `capitalCalledGhs`,
  `uncalledCapitalGhs`, `distributionsReceivedGhs`, `currentNavGhs`,
  `totalReturnGhs`, `irr` (optional, decimal), `equityMultiple` (optional),
  `firstInvestmentDate`, `investorClass` (`retail` | `sophisticated` |
  `institutional` | `diaspora`), `kycStatus` (`pending` | `cleared` | `expired`
  | `flagged`), `distributionBankAccount` (optional
  `{ bankName, accountNumber, sortCode, currency: GHS|USD|GBP|EUR }`),
  `isActive`.
- **`EquityWaterfall`** — JV distribution waterfall. `id`, `fundId`, `name`,
  `preferredReturnRate` (per-annum decimal), `isCumulative`, `tiers` (ordered
  array of `{ tierNumber, name, description, lpShare, gpShare, irrHurdle? }`),
  `hasCatchUp` (GP catch-up provision), `returnOfCapitalFirst`, `waterfallType`
  (`american` | `european`), `targetIrr`, `createdAt`, `updatedAt`.
- **`CapitalCall`** — `id`, `fundId`, `callNumber`, `issuedDate`, `dueDate`,
  `totalAmountCalledGhs`, `purpose`, `investmentReferences`, `investorCalls`
  (per-investor
  `{ investorId, allocationId, amountCalledGhs, status: pending|paid|overdue|defaulted|excused, paidDate?, paymentReference? }`),
  `status` (`draft` | `issued` | `partially_funded` | `fully_funded` |
  `cancelled`), `noticeDocUrl` (optional), `createdBy`, `createdAt`.
- **`CrowdfundingCampaign`** — `id`, `propertyId`, `projectId` (optional),
  `title`, `description`, `targetAmountGhs`, `minimumInvestmentGhs`,
  `maximumInvestmentGhs` (optional regulatory cap), `expectedAnnualReturn`
  (decimal), `horizonMonths`, `startDate`, `closingDate`, `raisedAmountGhs`,
  `investorCount`, `status` (`draft` | `open` | `overfunded` | `funded` |
  `cancelled` | `completed`), `secApprovalReference` (optional, SEC Ghana),
  `offeringMemorandumUrl` (optional), `updates` (array of
  `{ date, title, body }`), `createdAt`, `updatedAt`.
- **`DiasporaInvestment`** — `id`, `investorId`, `residenceCountry` (ISO 3166-1
  alpha-2), `sourceCurrency` (`GBP` | `USD` | `EUR` | `CAD` | `AUD` | `JPY` |
  `CHF` | `NOK` | `SEK`), `investmentType` (`direct_property` | `reit` |
  `crowdfunding` | `off_plan` | `land_banking`), `propertyId`/
  `fundId`/`campaignId` (optional), `amountSourceCurrency`, `exchangeRate`,
  `amountGhs`, `remittanceChannel` (`bank_transfer` | `mtn_momo` |
  `vodafone_cash` | `western_union` | `wise` | `remitly` | `ghipss`),
  `remittanceReference`, `fundsReceivedDate`, `fxHedgeReference` (optional),
  `amlChecks`
  (`{ sourceOfFundsVerified, sourceOfFundsDocument?, pepScreeningClear, sanctionsScreeningClear, checkedDate, checkedBy }`),
  `graDeclarationRef` (optional, Ghana Revenue Authority), `bogApprovalRef`
  (optional, Bank of Ghana — required for remittances
  > USD 50,000), `status` (`pending_compliance` | `active` | `matured` |
  > `exited` | `cancelled`), `createdAt`, `updatedAt`.

### 2.17 Manufacturing and materials supplementary types

These types model the full factory production workflow. A `FactoryOrder` is a
multi-line production order covering any mix of product types.
`FurnitureProduct` and `TileProduct` extend the materials catalog with
product-specific dimensions and certifications. `RawMaterialSource` tracks the
supplier side of procurement. `ProductionBatch` records a single manufacturing
run with yield calculation. `QualityTestResult` records an individual lab test
result referenced to an international standard.

- **`FactoryOrder`** — `id`, `customerId`, `projectId` (optional), `lineItems`
  (array of
  `{ productId, productType: prefab_module| concrete_block|steel|paint|tile|furniture|other, quantity, unit, unitCostGhs, lineValueGhs }`),
  `totalValueGhs`, `requestedDeliveryDate`,
  `committedDeliveryDate`/`actualDeliveryDate` (optional), `qualityGrade`
  (`standard` | `premium` | `bespoke`), `schedule` (array of
  `{ stage: design_confirmation|material_procurement|fabrication|qc_testing|finishing| dispatch, plannedStart, plannedEnd, actualEnd?, status: pending| in_progress|completed|delayed }`),
  `status` (`draft` | `confirmed` | `in_production` | `qc_hold` |
  `ready_for_dispatch` | `dispatched` | `delivered` | `cancelled`),
  `deliveryAddress` (`GhanaAddress`), `notes` (optional), `createdAt`,
  `updatedAt`.
- **`FurnitureProduct`** — `id`, `name`, `designCode`, `category` (`bedroom` |
  `living_room` | `dining` | `office` | `outdoor` | `kitchen` | `storage` |
  `bathroom`), `bom` (bill of materials array), `dimensions`
  (`{ widthMm, depthMm, heightMm }`), `weightKg`, `primaryFinish`,
  `finishOptions`, `unitCostGhs`, `retailPriceGhs`, `assemblyInstructionsUrl`
  (optional), `gsaCertified`, `leadTimeDays`, `isActive`.
- **`TileProduct`** — `id`, `name`, `productCode`, `dimensions`
  (`{ widthMm, lengthMm, thicknessMm }`), `material` (`ceramic` | `porcelain` |
  `granite` | `marble` | `slate` | `travertine` | `mosaic`), `finish`
  (`polished` | `matte` | `satin` | `textured` | `rustic` | `wood_look`),
  `colorFamily`, `slipRating` (EN ISO 10545-17: `A` | `B` | `C` | `R9`–`R13`),
  `waterAbsorptionPct` (EN ISO 10545-3), `breakingStrengthN` (EN ISO 10545-4),
  `suitableFor` (array of `floor` | `wall` | `outdoor` | `wet_area` |
  `heavy_traffic` | `commercial`), `coveragePerBoxM2`, `piecesPerBox`,
  `pricePerM2Ghs`, `supplierName`, `countryOfOrigin`, `gsaCertified`,
  `isActive`.
- **`RawMaterialSource`** — `id`, `material`, `supplierName`, `supplierContact`,
  `origin`, `unit`, `unitCostGhs`, `priceVolatilityNote` (optional),
  `leadTimeDays`, `minimumOrderQty`, `qualityGrade`, `certification` (optional),
  `monthlyCapacityUnits`, `reliabilityScore` (0–100, historical delivery
  performance), `isPreferredSupplier`, `isActive`, `lastPriceReviewDate`.
- **`ProductionBatch`** — `id`, `factoryOrderId`, `productId`, `productType`,
  `batchNumber`, `plannedQuantity`, `actualQuantity`, `rejectedQuantity`,
  `yieldPct` (`(actual − rejected) / planned × 100`), `startTime`, `endTime`,
  `productionLine`, `supervisedBy`, `materialsConsumed` (array of
  `{ rawMaterialId, materialName, quantityUsed, unit }`), `qcTestIds`, `status`
  (`in_production` | `qc_testing` | `passed` | `failed` | `on_hold` |
  `scrapped`), `notes` (optional).
- **`QualityTestResult`** — `id`, `batchId`/`materialDeliveryId` (optional),
  `testType` (`compressive_strength` | `tensile_strength` | `water_absorption` |
  `slump_test` | `gradation` | `moisture_content` | `surface_hardness` |
  `impact_resistance` | `dimensional_check` | `coating_thickness` |
  `chemical_composition` | `fire_resistance` | `visual_inspection`),
  `specimenId`, `standardReference` (e.g. `BS EN 12390-3`, `ASTM C39`,
  `GS ISO 6892-1`), `measuredValue`, `unit`, `minimumRequired`/`maximumAllowed`
  (optional), `result` (`pass` | `fail` | `marginal`), `laboratoryName`,
  `laboratoryAccreditation` (optional, e.g. `GNBS`, `UKAS`), `testDate`,
  `testedBy`, `certificateUrl` (optional), `notes` (optional).

### 2.18 Ghana Lands Commission (GLC) integration types

The Ghana Lands Commission is the statutory authority responsible for land
administration and title registration in Ghana. Cybele integrates with the GLC
API to perform title searches, verify parcel boundaries, check encumbrances, and
register deeds. The types below model every request, response, and webhook event
in that integration. `GlcParcelVerification` is the aggregate KYC/AML result
that site analysis uses to flag parcels as `clean`, `minor_issues`,
`major_issues`, or `do_not_proceed`.

`types.ts` defines a dedicated block of interoperability types for the Ghana
Lands Commission API (title searches, parcel verification, deed registration,
encumbrance checks):

- **`GlcApiCredentials`** — `apiKey`, `bearerToken` (optional OAuth 2.0 bearer,
  supersedes `apiKey`), `clientId`.
- **`GlcTitleType`** — `freehold` | `leasehold` | `stool_land` | `family_land` |
  `vested_land` | `concession`.
- **`GlcStoolLandRegion`** — `GREATER_ACCRA` | `ASHANTI` | `EASTERN` | `CENTRAL`
  | `WESTERN` | `VOLTA` | `BRONG_AHAFO` | `NORTHERN` | `UPPER_EAST` |
  `UPPER_WEST` (Office of the Administrator of Stool Lands regions).
- **`GlcSearchStatus`** — `pending` | `in_review` | `completed` | `expired`
  (result older than 3 months) | `disputed` (court caveat/injunction) |
  `not_found`.
- **`GlcTitleSearchRequest`** — `titleNumber`, `region`, `purpose`
  (`purchase_due_diligence` | `mortgage_security` | `litigation_support` |
  `internal_audit`), `requestingOrganisation`, `parcelId` (optional).
- **`GlcEncumbrance`** — `type` (`mortgage` | `caveat` | `caution` |
  `restriction` | `court_order` | `lease_notation`), `holder`,
  `principalAmountGhs` (optional), `lodgedDate`, `expiryDate` (optional),
  `instrumentNumber`, `notes` (optional).
- **`GlcParcelBoundary`** — `parcelId`, `officialAreaSqm`, `boundary` (GeoJSON
  `Polygon`, WGS84/EPSG:4326), `centroidGng` (optional Ghana National Grid
  EPSG:25000 `{ easting, northing }`), `lastSurveyDate`, `surveyPlanRef`.
- **`GlcOwnershipRecord`** — `ownerName`, `ownerId` (masked Ghana Card/TIN),
  `registrationDate`, `disposedDate` (`string | null`), `acquisitionMethod`
  (`purchase` | `gift` | `inheritance` | `court_order` | `government_grant`),
  `instrumentNumber`.
- **`GlcTitleSearchResult`** — `searchReference`, `titleNumber`, `status`,
  `titleType`, `firstRegistrationDate`, `lastUpdatedDate`, `resultDate`,
  `expiryDate` (3-month validity), `currentOwners`, `ownershipHistory`,
  `encumbrances`, `parcel` (optional), `hasPendingApplications`,
  `isGeoreferenced`, `certificatePdfUrl` (optional), `digitalSignature`
  (optional JWS compact serialisation).
- **`GlcDeedRegistrationRequest`** — `titleNumber`, `instrumentType` (`transfer`
  | `mortgage` | `lease` | `discharge` | `caveat` | `power_of_attorney`),
  `grantor`/`grantee` (`{ name, id }`), `considerationGhs` (optional),
  `executionDate`, `documentBase64`, `documentHash` (SHA-256), `witnesses`,
  `solicitor` (optional).
- **`GlcDeedRegistrationResponse`** — `applicationNumber`, `status` (`submitted`
  | `under_review` | `queued_for_execution` | `registered` | `rejected`),
  `estimatedCompletionDate` (optional), `stampDutyGhs`/ `processingFeeGhs`
  (optional), `rejectionReason` (optional), `acknowledgementPdfUrl` (optional).
- **`GlcWebhookEvent`** — `eventType` (`title_updated` | `encumbrance_lodged` |
  `encumbrance_discharged` | `ownership_transferred`), `titleNumber`,
  `eventTimestamp`, `transactionReference`, `signature` (HMAC-SHA256),
  `summary`.
- **`GlcParcelVerification`** — aggregate KYC/AML result. `parcelId`,
  `titleNumber`, `boundaryVerified`, `areaDeltaSqm` (positive = seller
  overstated), `hasUnresolvedDisputes`, `hasUnregisteredEncumbrances`,
  `riskRating` (`clean` | `minor_issues` | `major_issues` | `do_not_proceed`),
  `issues` (array of `{ code, severity: info|warning|critical, description }`).

---

## 3. Persistence Schema

Source: `libs/cybele/db/src/schema.ts` (Drizzle ORM, `pg-core`). The schema
targets PostgreSQL with PostGIS (a custom `geometry(Polygon,4326)` column type,
`cybelePostgisPolygon`, is defined). All tables are exported in a single
`schema` object alongside their `relations`. Decimal precision constants: area
`numeric(10,2)` / plot area `numeric(12,2)`, money `numeric(14,2)`.

The database schema name is `cybele`. The section is organized by functional
cluster: §3.1 lists all PostgreSQL enum types (which appear in column
definitions below); §3.2–3.10 cover each table group; §3.11 describes the
Drizzle relation definitions; §3.12–3.13 document connection pooling and Redis
configuration.

### 3.1 PostgreSQL enum types (`pgEnum`)

Cybele defines database-native enum types so that column values are validated by
PostgreSQL itself, not only by application code. The enum names follow the
`cybele_` prefix convention. The following enum types are defined in
`schema.ts`; values that are non-obvious are noted inline:

`cybele_prop_status`, `cybele_prop_type`, `cybele_construction_status`,
`cybele_lease_type`, `cybele_escalation_type`, `cybele_ghana_region` (17
values), `cybele_zoning_class`, `cybele_land_tenure`,
`cybele_maintenance_priority` (`critical`, `high`, `medium`, `low`),
`cybele_inspection_category` (11 values), `cybele_mat_category` (`Cement`,
`Steel`, `Timber`, `Aggregates`, `Blocks`, `Roofing`, `Electrical`, `Plumbing`,
`Finishing`, `Prefab`, `Other`), `cybele_mat_qc_status` (`Pending`, `Passed`,
`Failed`, `OnHold`, `Quarantined`), `cybele_reit_fund_type` (`Equity`,
`Mortgage`, `Hybrid`, `Infrastructure`), `cybele_reit_fund_status`
(`Fundraising`, `Investing`, `Operating`, `Divesting`, `Closed`, `Wound_Up`),
`cybele_room_type` (`Standard`, `Deluxe`, `Suite`, `Executive`, `Presidential`,
`Studio`, `Serviced_Apartment`, `Dormitory`), `cybele_booking_status`
(`Inquiry`, `Provisional`, `Confirmed`, `Checked_In`, `Checked_Out`, `No_Show`,
`Cancelled`), `cybele_infra_type` (14 values: `Trunk_Road`, `Feeder_Road`,
`Urban_Road`, `Bridge`, `Culvert`, `Drainage`, `Water_Supply`, `Sewerage`,
`Power_Transmission`, `Telecoms`, `Airport`, `Port`, `Railway`, `Dam`),
`cybele_infra_project_status` (`Feasibility`, `Design`, `Procurement`,
`Construction`, `Commissioning`, `Operational`, `Decommissioned`),
`cybele_road_surface` (`Bitumen_Surface_Dressing`, `Asphalt_Concrete`,
`Concrete`, `Gravel`, `Laterite`, `Earth`).

### 3.2 Property, plot, building, unit tables

The property cluster stores the core built-environment hierarchy. The base
`cybele_properties` table is extended by type-specific tables via 1:1 FK
relationships, mirroring the TypeScript discriminated union in §2.6. Spatial
data lives in `cybele_plots` as both a GeoJSON JSONB column and a PostGIS
`geometry(Polygon,4326)` column (the latter is indexed for spatial queries).
Buildings subdivide into floors (`cybele_building_floors`), floor plans
(`cybele_floor_plans`), units, and common areas.

- **`cybele_properties`** — base property record. `id` (uuid PK,
  `defaultRandom`), `property_type`, `status`, `address_json` (jsonb, not null),
  `location_json` (jsonb), `gross_area_sqm`, `net_area_sqm`,
  `current_value_ghs`, `title_id`, `year_built`, `description`, `amenities`
  (text[]), `images` (text[]), `created_at`, `updated_at`.
- **`cybele_residential_properties`** — 1:1 extension keyed by `property_id` FK
  → `cybele_properties`. `bedrooms`, `bathrooms`, `floor_area_sqm`,
  `has_garden`, `parking_spaces`, `has_pool`, `furnishing`, `target_market`.
- **`cybele_commercial_properties`** — 1:1 extension. `office_grade`,
  `floor_plates_sqm`, `column_spacing_m`, `ceiling_height_m`, `loading_docks`,
  `generator_capacity_kva`, `fibre_connected`.
- **`cybele_plots`** — `id` (uuid PK), `title_id`, `area_sqm`,
  `boundary_geojson` (jsonb), `boundary_geometry` (PostGIS
  `geometry(Polygon,4326)`), `zoning`, `topography`, `soil_type`, `is_serviced`,
  `tenure`, `price_ghs`, `price_per_sqm`.
- **`cybele_buildings`** — `id` (uuid PK), `property_id` FK, `name`, `floors`,
  `total_units`, `gross_area_sqm`, `net_leasable_area_sqm`, `structural_system`,
  `year_built`, `condition`, `has_lift`, `has_backup_power`, `has_borehole`.
- **`cybele_building_floors`** — `id`, `building_id` FK, `level_number`,
  `level_name`, `usage_type`, `gross_area_sqm`, `net_area_sqm`, `elevation_m`,
  `spatial_path` (hierarchical path string), `sort_order`.
- **`cybele_floor_plans`** — `id`, `building_id` FK, `floor_id` FK (→
  `cybele_building_floors`), `plan_type`, `version`, `drawing_ref`,
  `storage_uri`, `file_format`, `scale`, `source_system`, `geometry_json`
  (jsonb), `is_current`, `issued_at`, `superseded_at`.
- **`cybele_units`** — `id`, `building_id` FK, `floor_id` FK, `unit_number`,
  `floor`, `area_sqm`, `unit_type`, `status`, `current_rent_ghs`,
  `market_rent_ghs`, `tenant_id`, `spatial_path`, `centroid_json`,
  `boundary_json`.
- **`cybele_common_areas`** — `id`, `building_id` FK, `floor_id` FK, `name`,
  `area_type`, `area_sqm`, `access_level`, `maintained_by`, `spatial_path`,
  `boundary_json`.
- **`cybele_land_titles`** — `id`, `title_number` (unique, not null),
  `registration_date`, `owner_id`, `owner_name`, `plot_id` FK (→
  `cybele_plots`), `encumbrances` (text[]), `area_sqm`, `survey_plan_ref`,
  `issue_authority`, `expiry_date`, `is_disputed`.

### 3.3 Construction tables

The construction cluster stores projects, their WBS breakdown, milestones, and
quality inspection records. The `cybele_wbs_items` table is self-referencing
(`parent_item_id`) to represent the WBS tree. Earned-value fields
(`budgeted_cost_ghs`, `earned_value_ghs`, `actual_cost_ghs`) are stored at the
WBS item level for detailed EVM reporting.

- **`cybele_construction_projects`** — `id`, `name`, `project_type`, `status`
  (`cybele_construction_status`), `client_id`, `property_id` FK, `location_json`
  (jsonb, not null), `contract_value_ghs` (not null),
  `revised_contract_value_ghs`, `start_date`, `planned_completion_date` (not
  null), `forecast_completion_date`, `percent_complete` (real, default 0),
  `project_manager` (not null), `main_contractor_id`, `consultants` (text[]),
  `created_at`, `updated_at`.
- **`cybele_project_phases`** — `id`, `project_id` FK, `phase_name`,
  `phase_status`, `planned_start`, `planned_end`, `actual_start`, `actual_end`,
  `percent_complete`, `cost_ghs`.
- **`cybele_wbs_items`** — work breakdown structure tasks. `id`, `project_id`
  FK, `phase_id` FK, `parent_item_id` (self-reference for the WBS tree),
  `wbs_code`, `name`, `description`, `item_type`, `status`, `responsible_org`,
  `planned_start`/`planned_end`/`actual_start`/ `actual_end`, `duration_days`,
  `budgeted_cost_ghs`, `actual_cost_ghs`, `earned_value_ghs`, `percent_complete`
  (real, default 0), `predecessors_json` (jsonb), `metadata_json` (jsonb),
  `created_at`, `updated_at`.
- **`cybele_milestones`** — `id`, `phase_id` FK, `name`, `planned_date`,
  `actual_date`, `is_critical_path`, `linked_payment_ghs`.
- **`cybele_quality_inspections`** — `id`, `project_id` FK, `category`
  (`cybele_inspection_category`), `location`, `inspection_date`, `inspector`,
  `results_json` (jsonb, not null), `overall_status`, `ncrs` (text[]).

### 3.4 Tenant, lease, rent, maintenance tables

The tenant cluster stores the full rental relationship. `cybele_leases` links a
tenant to a unit; `cybele_rent_schedules` is the period-by-period billing ledger
derived from a lease; `cybele_rent_payments` records each received payment with
its mobile-money or bank channel. `cybele_maintenance_tickets` is the service
request log, with SLA target dates computed from priority at creation time
(critical = 1 day, high = 3 days, medium = 7 days).

- **`cybele_tenants`** — `id`, `tenant_type`, `name`, `contact_email`,
  `contact_phone` (not null), `id_type`, `id_number`, `address`,
  `employer_name`, `monthly_income_ghs`, `credit_score`, `rental_history`,
  `created_at`, `is_active` (default true).
- **`cybele_leases`** — `id`, `unit_id` FK, `tenant_id` FK, `lease_type`
  (`cybele_lease_type`), `start_date`, `end_date`, `rent_amount_ghs` (not null),
  `rent_currency` (default `GHS`), `escalation_type` (`cybele_escalation_type`),
  `escalation_rate` (real), `deposit_amount_ghs` (not null), `deposit_held` (not
  null), `renewal_notice_days` (default 90), `terms` (text[]), `is_active`
  (default true), `created_at`, `terminated_at`.
- **`cybele_rent_schedules`** — `id`, `lease_id` FK, `tenant_id` FK, `unit_id`
  FK, `period_number`, `period_start`, `period_end`, `due_date`, `amount_ghs`,
  `paid_ghs`, `balance_ghs`, `status`, `days_overdue` (default 0),
  `late_fee_ghs` (default `0`), `generated_at`, `last_payment_at`.
- **`cybele_rent_payments`** — `id`, `lease_id` FK, `schedule_id` FK,
  `tenant_id`, `amount_ghs` (not null), `channel`, `reference_number` (not
  null), `payment_date`, `status`, `note`, `confirmed_at`.
- **`cybele_maintenance_tickets`** — `id`, `property_id`, `unit_id`,
  `tenant_id`, `category`, `description` (not null), `priority`
  (`cybele_maintenance_priority`), `reported_at`, `target_completion_date` (not
  null), `assigned_vendor_id`, `status`, `actual_cost_ghs`,
  `estimated_cost_ghs`, `completed_at`, `notes`.

### 3.5 Market and PropTech tables

The market cluster stores the public-facing PropTech data: active property
listings, comparable-sale transactions used for valuation, competing
developments tracked for market intelligence, and mortgage applications.
`cybele_market_transactions` records are the comparable sales that the finance
modules adjust and index.

- **`cybele_property_listings`** — `id`, `title`, `description`,
  `property_type`, `transaction_type`, `price_ghs` (not null), `currency`
  (default `GHS`), `bedrooms`, `bathrooms`, `area_sqm` (not null), `district`,
  `region`, `latitude`, `longitude`, `amenities` (text[]), `images` (text[]),
  `agent_id` (not null), `status`, `published_at`, `view_count` (default 0),
  `inquiry_count` (default 0), `quality_score` (default 0).
- **`cybele_market_transactions`** — comparable sale records. `id`,
  `transaction_date`, `property_type`, `location`, `district`, `region`,
  `area_sqm`, `transaction_price_ghs`, `price_per_sqm`, `bedrooms`,
  `is_verified` (default false).
- **`cybele_competitor_developments`** — `id` (uuid PK, `defaultRandom`),
  `name`, `type`, `status`, `lat`/`lon` (`numeric(9,6)`), `region`, `scale`,
  `year_completed`, `source`, `created_at`/`updated_at` (timestamptz).
- **`cybele_mortgage_applications`** — `id`, `applicant_id`, `property_id`,
  `product_id`, `loan_amount_ghs`, `tenure_months`, `annual_rate_pct` (real),
  `gross_monthly_income_ghs`, `monthly_debt_obligations_ghs`, `credit_score`,
  `employment_type`, `status`, `approved_amount_ghs`, `approved_at`,
  `created_at`.

### 3.6 Industrial-park and prefab tables

Industrial-park plots subdivide a park into individually leasable land parcels
(distinct from the building `Unit` concept). Prefab production is tracked in
three linked tables: a module design catalog, individual production orders, and
per-module factory order items that trace through each production station.
On-site assembly tasks are linked back to the factory order item for full
traceability from module design to installed position.

- **`cybele_industrial_park_plots`** — `id`, `zone_id`, `area_m2`, `status`,
  `frontage_m`, `depth_m`, `monthly_lease_rate_ghs_per_sqm`, `tenant_id`,
  `tenant_name`, `allocated_power_kw`, `allocated_water_m3_per_day`.
- **`cybele_prefab_module_designs`** — `id`, `design_code` (unique),
  `module_type`, `version`, `description`, `external_dimensions_json` (jsonb,
  not null), `internal_dimensions_json` (jsonb), `finished_weight_kg`,
  `structural_capacity_kn_m2`, `stackable_floors`, `production_days` (not null),
  `unit_cost_ghs` (not null), `bom_json` (jsonb), `drawing_refs` (text[]),
  `assembly_instructions_url`, `is_active`, `created_at`, `updated_at`.
- **`cybele_prefab_orders`** — `id`, `design_id` FK, `module_type`, `quantity`,
  `design_code`, `customer_name`, `delivery_date`, `priority`, `status`,
  `unit_cost_ghs`, `start_date`, `completed_date`.
- **`cybele_prefab_factory_order_items`** — per-module production line items.
  `id`, `order_id` FK, `design_id` FK, `module_serial`, `sequence_number`,
  `station`, `production_status`, `started_at`, `completed_at`, `qc_status`,
  `dispatch_ready_at`.
- **`cybele_prefab_assembly_tasks`** — on-site assembly steps. `id`, `order_id`
  FK, `order_item_id` FK, `assembly_step`, `task_name`, `status`,
  `planned_start`/`planned_end`/`actual_start`/`actual_end`, `crew_code`,
  `dependency_json` (jsonb), `percent_complete` (real, default 0),
  `inspection_status`, `notes`.

### 3.7 Materials tables (phase anchor 58.1.2.6)

The materials cluster stores the catalog, warehouse stock levels, production
batches, and QC test results. The `cybele_material_inventory` table is
per-warehouse, so a single material item can have stock tracked across multiple
locations simultaneously. `cybele_material_batches` enables batch traceability;
`cybele_material_qc_records` stores individual test results against a batch
using international test standards (e.g. `BS EN 12390`, `ASTM C39`).

- **`cybele_material_items`** — master catalogue. `id`, `sku` (unique), `name`,
  `category` (`cybele_mat_category`), `unit_of_measure`, `specification`,
  `ghana_standard` (GSA code), `supplier_id`, `unit_cost_ghs` (not null),
  `lead_time_days` (default 7), `minimum_order_qty` (default `1`), `created_at`,
  `updated_at`.
- **`cybele_material_inventory`** — per-warehouse stock. `id`, `material_id` FK,
  `location_code`, `quantity_on_hand`, `quantity_reserved`, `quantity_on_order`,
  `reorder_point`, `valuation_ghs`, `last_count_date`, `updated_at`.
- **`cybele_material_batches`** — manufactured/sourced batch records for
  traceability. `id`, `material_id` FK, `batch_number` (unique),
  `manufacture_date`, `expiry_date`, `quantity_produced`, `production_line`,
  `raw_material_cost_ghs`, `labor_cost_ghs`, `overhead_cost_ghs`, `qc_status`
  (`cybele_mat_qc_status`, default `Pending`), `certification_ref`, `notes`,
  `created_at`.
- **`cybele_material_qc_records`** — `id`, `batch_id` FK, `tested_by`,
  `test_date`, `test_type`, `test_standard` (e.g. `BS EN 12390`, `ASTM C39`),
  `result`, `unit`, `min_acceptable`, `max_acceptable`, `status`
  (`cybele_mat_qc_status`), `lab_reference`, `report_url`, `created_at`.

### 3.8 REIT / fund tables (phase anchor 58.1.2.9)

The REIT cluster stores real-estate investment trust structures. A
`cybele_reit_funds` record is the top-level fund entity, referencing its SEC
Ghana registration. `cybele_reit_portfolio_items` links fund records to
underlying property assets. `cybele_reit_nav_snapshots` records periodic NAV
computations (NAV = gross asset value minus total liabilities). Investor records
track KYC status and distributions received.

- **`cybele_reit_funds`** — `id`, `name`, `ticker` (unique), `fund_type`
  (`cybele_reit_fund_type`), `status` (`cybele_reit_fund_status`, default
  `Fundraising`), `target_size_ghs`, `raised_ghs` (default `0`),
  `inception_date`, `maturity_date`, `distribution_frequency` (default
  `quarterly`), `target_distribution_yield_pct`, `management_fee_pct` (default
  `1.5`), `performance_fee_pct` (default `20`), `custodian`, `trustee`,
  `sec_registration_ref` (SEC Ghana), `prospectus_url`, `created_at`,
  `updated_at`.
- **`cybele_reit_portfolio_items`** — `id`, `fund_id` FK, `property_id` FK,
  `acquisition_date`, `acquisition_cost_ghs`, `current_valuation_ghs`,
  `ownership_pct` (default `100`), `annual_noi_ghs`, `last_valuation_date`,
  `valuation_method`, `encumbered_ghs` (mortgage debt on the asset, default
  `0`).
- **`cybele_reit_nav_snapshots`** — `id`, `fund_id` FK, `snapshot_date`,
  `gross_asset_value_ghs`, `total_liabilities_ghs`, `nav_ghs` (GAV −
  liabilities), `units_outstanding`, `nav_per_unit_ghs` (NAV / units),
  `dividend_paid_ghs` (default `0`), `management_fee_ghs`, `computed_by`,
  `notes`, `created_at`.
- **`cybele_reit_investors`** — `id`, `fund_id` FK, `investor_name`,
  `investor_type`, `ghana_card_or_tin`, `units_held`, `subscription_price_ghs`,
  `subscription_date`, `total_invested_ghs`, `total_distributions_ghs` (default
  `0`), `bank_account_ref`, `kyc_status` (default `Pending`), `created_at`.

### 3.9 Hospitality tables (phase anchor 58.1.2.11)

The hospitality cluster extends the base property record with hotel-specific
operational data. A `cybele_hospitality_properties` record (1:1 with a
`cybele_properties` record) carries PMS system references and channel manager
details. Room inventory and dynamic rate calendars are stored separately so rate
overrides can be managed without touching the room record. Booking confirmation
numbers are unique across the table for guest reference.

- **`cybele_hospitality_properties`** — `id`, `property_id` FK (unique),
  `star_rating`, `brand_name`, `total_rooms` (not null), `meeting_room_count`
  (default 0), `restaurant_seats` (default 0), `pool_available`,
  `gym_available`, `spa_available`, `pms_system`, `channel_manager_ref`,
  `check_in_time` (default `14:00`), `check_out_time` (default `12:00`).
- **`cybele_room_inventory`** — `id`, `hospitality_property_id` FK,
  `room_number`, `floor`, `room_type` (`cybele_room_type`), `max_occupancy`
  (default 2), `size_sqm`, `bed_configuration`, `connecting_room`,
  `is_accessible`, `is_active`, `amenities_json` (jsonb), `base_rate_ghs`.
- **`cybele_rate_calendar`** — daily/seasonal rate overrides per room type.
  `id`, `hospitality_property_id` FK, `room_type`, `valid_from`, `valid_to`,
  `rate_name` (`rack`, `corporate`, `group`, `advance_purchase`, …),
  `rate_ghs_per_night`, `breakfast_included`, `min_night_stay` (default 1),
  `advance_purchase_days`, `is_active`, `created_at`.
- **`cybele_bookings`** — `id`, `confirmation_no` (unique),
  `hospitality_property_id` FK, `room_id` FK, `guest_name`, `guest_email`,
  `guest_phone`, `guest_nationality`, `status` (`cybele_booking_status`, default
  `Inquiry`), `check_in_date`, `check_out_date`, `adults` (default 1),
  `children` (default 0), `rate_ghs_per_night`, `total_nights`,
  `total_amount_ghs`, `deposit_paid_ghs` (default `0`), `balance_due_ghs`,
  `channel_source`, `special_requests`, `booked_at`, `updated_at`.

### 3.10 Civil infrastructure tables (phase anchor 58.1.2.12)

Civil infrastructure projects differ from building projects in that they have
linear assets (roads measured in chainages), governmental clients (`client_name`
e.g. GHA, GWCL, ECG), environmental impact permit references, and pavement
condition indices. Road segments store AASHTO structural numbers and California
Bearing Ratio values — the engineering parameters that determine pavement design
life. Maintenance logs track post-maintenance PCI scores to measure intervention
effectiveness.

- **`cybele_civil_infra_projects`** — `id`, `name`, `project_code` (unique),
  `infra_type` (`cybele_infra_type`), `status` (`cybele_infra_project_status`,
  default `Feasibility`), `region` (`cybele_ghana_region`), `district`,
  `contractor_name`, `consultant_name`, `client_name` (not null — e.g. GHA,
  GWCL, ECG), `contract_value_ghs`, `funding_source` (GoG, World Bank, AfDB, …),
  `commencement_date`, `original_completion_date` (not null),
  `revised_completion_date`, `actual_completion_date`, `length_km`, `area_ha`,
  `design_life_years`, `environ_impact_ref` (EPA Ghana permit reference),
  `created_at`, `updated_at`.
- **`cybele_road_segments`** — `id`, `project_id` FK, `segment_code`,
  `description`, `start_chainage`/`end_chainage` (km), `length_km`,
  `carriagewidth_m`, `lanes` (default 2), `surface_type`
  (`cybele_road_surface`), `design_speed` (km/h), `structural_number` (AASHTO
  SN), `cbr` (subgrade California Bearing Ratio %), `adt` (Average Daily
  Traffic, vehicles/day), `pci_score` (Pavement Condition Index 0–100),
  `last_survey_date`, `construction_status` (default `Not_Started`),
  `completion_pct` (default `0`), `created_at`, `updated_at`.
- **`cybele_infra_maintenance_logs`** — `id`, `project_id` FK, `segment_id` FK,
  `maintenance_type` (`routine`, `periodic`, `rehabilitation`, `emergency`),
  `priority` (`cybele_maintenance_priority`), `description`, `scheduled_date`,
  `completed_date`, `contractor`, `estimated_cost_ghs`, `actual_cost_ghs`,
  `funding_ref`, `defects_rectified` (text[]), `post_maintenance_pci` (real),
  `supervising_engineer`, `notes`, `created_at`.

### 3.11 Relations

Drizzle `relations` declarations in `schema.ts` enable type-safe query
composition across the table groups above. The following relation chains are
defined — each represents a join path that application queries can traverse:

`schema.ts` declares Drizzle `relations` joining the major aggregates: property
→ residential/commercial details, buildings, construction projects; building →
floors, floor plans, units, common areas; construction project → phases, WBS
items, inspections; phase → WBS items, milestones; tenant → leases, rent
schedules, rent payments; lease → unit, tenant, rent schedules, payments; plot ↔
land titles; material item → inventory, batches; batch → QC records; prefab
module design → orders, factory order items; prefab order → factory order items,
assembly tasks; REIT fund → portfolio items, NAV snapshots, investors;
hospitality property → rooms, rate calendar, bookings; civil infra project →
road segments, maintenance logs.

### 3.12 Connection pooling (phase anchor 58.1.2.21)

Different service roles have different connection requirements. A construction
update that holds a transaction open needs a `session` pool mode; a read-only
analytics query can use `transaction` mode. Cybele defines per-role pool
profiles so each service opens only the connections it needs without starving
others.

Source: `libs/cybele/db/src/connection.ts`. PgBouncer-aware pool profiles are
defined per `ServiceRole`: `ecommerce`, `construction`, `property`, `analytics`,
`iot`, `hospitality`, `finance`, `admin`. Each `PoolProfile` carries an
application-side `max` connection count, a PgBouncer `pgbMaxPool`
server-connection size, and a PgBouncer pool `mode` (`transaction`, `session`,
or `statement`). The pooling design assumes a PostgreSQL `max_connections` of
200 in production.

### 3.13 Redis caching and pub/sub

Redis serves two roles in Cybele: caching frequently-read records to reduce
database load, and pub/sub for real-time notifications between service
instances. All keys are namespaced under `cybele:` and TTLs are calibrated to
the read/write frequency of each entity (listings change more often than price
indices, so their TTLs are shorter).

Source: `libs/cybele/db/src/redis-config.ts`. Cybele uses Redis (key prefix
`cybele`) for read-through caching and pub/sub.

- **`CybeleRedisKeys`** — keyed accessors: `property`, `propertyList`,
  `listing`, `searchResults`, `priceIndex`, `constructionProject`,
  `leaseByUnit`, `tenantProfile`, `agentSession`, `marketSnapshot`,
  `maintenanceQueue`.
- **`CybeleRedisTTL`** (seconds) — `PROPERTY` 600, `LISTING` 300, `SEARCH` 120,
  `PRICE_INDEX` 3600, `MARKET_SNAPSHOT` 1800, `AGENT_SESSION` 86400,
  `TENANT_PROFILE` 900, `CONSTRUCTION_PROJECT` 300.
- **`CybelePubSubChannels`** — `PROPERTY_UPDATE`, `LISTING_PUBLISHED`,
  `RENT_RECEIVED`, `MAINTENANCE_RAISED`, `PROJECT_MILESTONE` (all under the
  `cybele:events:*` namespace).

---

## 4. API Surface

The API layer is built on [Hono](https://hono.dev/) and assembled in
`@cybele/api`. Subsections 4.1–4.5 cover the core REST routes, auth, and rate
limiting. Subsections 4.6–4.10 cover the auxiliary protocols (GraphQL, gRPC,
WebSocket). Section 4.11 covers the deployable API gateway app's own management
endpoints. Section 4.12 describes the four non-gateway application services'
domain layers.

### 4.1 Gateway (`@cybele/api`)

Source: `libs/cybele/api/src/gateway.ts`. `createGateway(config)` builds a Hono
application mounted at the configurable `basePath` (default `/api/v1`).
`GatewayConfig` accepts `basePath`, `corsOrigins`, `timeoutMs` (default 30000),
`enableAuth` (default true), `enableRateLimit` (default true), `enableMetrics`
(default true), `jwtSecret`, and `redisUrl` (default `redis://localhost:6379`).

Middleware pipeline, in order: `secureHeaders` → `cors` → `requestId` →
structured JSON logging → OpenTelemetry tracing → Prometheus request metrics →
request `timeout` → per-IP sliding-window rate limiter → JWT auth → (dev only)
Hono request logger. Public paths bypassing JWT auth: `<basePath>/health`,
`<basePath>/metrics`, `<basePath>/listings`, `<basePath>/market-intel/prices`.
The gateway mounts four REST route groups (`/properties`, `/construction`,
`/leases`, `/materials`) plus `/health` and `/metrics`. A 404 fallback returns
code `CYBELE_404`; the global error handler returns code `CYBELE_500` with the
request ID.

`CYBELE_SERVICE_REGISTRY` is the service dispatch table mapping a route prefix
to a downstream service descriptor (`name`, `routePrefix`, `healthPath`):
`property` (`/properties`), `construction` (`/construction`), `lease`
(`/leases`), `tenant` (`/tenants`), `materials` (`/materials`), `listings`
(`/listings`, service `cybele-proptech`), `finance` (`/finance`), `market-intel`
(`/market-intel`).

### 4.2 Property REST routes (`routes/properties.ts`, anchor 58.1.3.2)

The property routes provide CRUD over the property hierarchy plus valuation.
Each route is guarded by a `requirePermission(...)` middleware that checks the
caller's role against the RBAC matrix (§4.6). The route layer uses an injectable
`PropertyRepository`; in production, `DrizzlePropertyRepository` issues real SQL
through `@cybele/db`.

All routes live under `/api/v1/properties`:

| Method | Path             | Permission        | Purpose                                    |
| ------ | ---------------- | ----------------- | ------------------------------------------ |
| GET    | `/`              | `property:read`   | List properties, filter + paginate         |
| POST   | `/`              | `property:write`  | Create a property                          |
| GET    | `/:id`           | `property:read`   | Get property by UUID                       |
| PUT    | `/:id`           | `property:write`  | Update property                            |
| DELETE | `/:id`           | `property:delete` | Soft-delete (sets `deletedAt`)             |
| GET    | `/:id/units`     | `property:read`   | List units in the property's building      |
| GET    | `/:id/valuation` | `property:read`   | Latest completed valuation                 |
| POST   | `/:id/valuation` | `property:write`  | Request an automated comparative valuation |

`CreatePropertySchema` validates `propertyType` (one of eight `PropertyType`
literals), `status` (one of seven non-`Demolished` literals), `address` (Ghana
address with the `XX-NNN-NNNN` digital-address regex), `location` (latitude
constrained to 4–12°N, longitude to −4–2°E — Ghana's bounding box),
`grossAreaSqm` (positive), `netAreaSqm`, `currentValueGHS`, `titleId`,
`yearBuilt` (1900 … current year + 5), `description` (10–5000 chars),
`amenities`, `images` (URLs). The list query supports `type`, `status`,
`region`, `district`, `minArea`/`maxArea`, `minValue`/`maxValue`, `page` (≥1),
`limit` (1–100, default 20), `sort` (`createdAt` | `currentValueGHS` |
`grossAreaSqm`), `order` (`asc` | `desc`). The route layer uses an injectable
`PropertyRepository`; `setPropertyRepository(...)` swaps the default
`InMemoryPropertyRepository` for a `DrizzlePropertyRepository` so handlers issue
real SQL through `@cybele/db`. The valuation estimator applies a
`purposeOfValuation`-dependent factor (0.97 for `mortgage`) and a
`valuationType` factor (0.82 for `forced_sale`), falling back to a region-based
price/m² (Greater Accra 5900, Ashanti 4500, else 3800 GHS/m²).

### 4.3 Construction REST routes (`routes/construction.ts`, anchor 58.1.3.3)

The construction routes drive a project from creation through completion. Three
mutations write signed events to the hash-chained construction ledger (§7):
`POST /` records `budget_committed`; `PUT /:id` records `progress_attested`;
`POST /:id/progress-claim` records `payment_settled`. The earned-value endpoint
derives its metrics from that ledger rather than from ephemeral state.

All routes live under `/api/v1/construction`:

| Method | Path                   | Permission           | Purpose                                                                        |
| ------ | ---------------------- | -------------------- | ------------------------------------------------------------------------------ |
| GET    | `/`                    | `construction:read`  | List projects, filter + paginate                                               |
| POST   | `/`                    | `construction:write` | Create project (records `budget_committed` on the ledger)                      |
| GET    | `/:id`                 | `construction:read`  | Get project with current earned-value metrics                                  |
| PUT    | `/:id`                 | `construction:write` | Update progress (records `progress_attested`)                                  |
| GET    | `/:id/phases`          | `construction:read`  | List WBS phases                                                                |
| POST   | `/:id/phases`          | `construction:write` | Add a phase with milestones                                                    |
| PUT    | `/:id/phases/:phaseId` | `construction:write` | Update phase progress; at 100% records `milestone_achieved` for each milestone |
| POST   | `/:id/milestones`      | `construction:write` | Record a milestone                                                             |
| GET    | `/:id/inspections`     | `construction:read`  | List QC inspections, filter by category                                        |
| POST   | `/:id/inspections`     | `construction:write` | Submit a QC inspection                                                         |
| GET    | `/:id/earned-value`    | `construction:read`  | EVM metrics, optional `?asOf=<iso>`                                            |
| POST   | `/:id/progress-claim`  | `construction:write` | Submit a payment claim (records `payment_settled`)                             |

`CreateProjectSchema` validates `name` (3–200 chars), `projectType`
(`residential` | `commercial` | `industrial` | `infrastructure` | `mixed_use`),
`propertyId` (optional UUID), `contractValueGHS` (positive),
`startDate`/`plannedCompletionDate` (datetimes; completion must be after start),
`projectManager`, `clientId`, `mainContractorId` (optional), `consultants`
(optional), `location` (`{ region, district, latitude?, longitude? }`).
`QualityInspectionSchema` constrains `category` to the 11 inspection categories
and `overallStatus` to `pass` | `conditional_pass` | `fail` | `pending_retest`.
`ProgressClaimSchema` carries `claimNumber`, `periodFrom`/`periodTo`,
`workDoneGHS`, `materialsOnSiteGHS`, `retentionPct` (0–10, default 5),
`variationOrders`, `submittedBy`; the handler computes gross claim, retention
deduction, approved-variation total, and net-certified amount.

### 4.4 Lease and tenant REST routes (`routes/leases.ts`, anchor 58.1.3.4)

The lease routes handle the full tenancy lifecycle — creation, renewal,
termination, payment recording, and arrears calculation. Maintenance tickets are
nested under leases because they are raised in the context of a tenancy. Tenant
routes are also grouped here. All routes live under `/api/v1/leases`.

**Lease routes:** `GET /` (list), `POST /` (create — `lease:write`), `GET /:id`
(with arrears), `PUT /:id`, `POST /:id/renew`, `POST /:id/terminate`,
`GET /:id/payments`, `POST /:id/payments`, `GET /:id/arrears`,
`POST /:id/escalate`.

**Tenant routes:** `GET /tenants`, `POST /tenants`, `GET /tenants/:id` (with
payment score), `PUT /tenants/:id`, `DELETE /tenants/:id` (deactivate),
`GET /tenants/:id/leases`, `GET /tenants/:id/score`.

**Maintenance routes (nested):** `POST /:id/maintenance` (`maintenance:write`),
`GET /:id/maintenance` (`maintenance:read`).

`CreateTenantSchema` validates `tenantType` (`individual` | `company` |
`government` | `ngo`), `contactPhone` (10–15 chars), `idType` (`ghana_card` |
`passport` | `drivers_license` | `tin` | `company_reg`), `creditScore`
(300–850). `LeaseShapeSchema` validates `unitId`/`tenantId` (UUIDs), `leaseType`
(six `LeaseType` literals), `escalationType` (`fixed_pct` | `cpi_linked` |
`market_review`), `escalationRate` (0–50%), `renewalNoticeDays` (30–365, default
90), `rentCurrency` (`GHS` | `USD` | `EUR` | `GBP`); `CreateLeaseSchema` refines
that `endDate` is after `startDate`. `RecordPaymentSchema` constrains `channel`
to `mtn_momo` | `vodafone_cash` | `airteltigo` | `bank_transfer` | `cheque` |
`cash`. `TerminateLeaseSchema.reason` is one of `mutual_agreement` |
`breach_by_tenant` | `breach_by_landlord` | `property_sold` |
`property_redevelopment` | `non_payment` | `tenant_relocation`.
`MaintenanceTicketSchema.priority` is `critical` | `high` | `medium` | `low`;
the handler sets `targetCompletionDate` to 1/3/7 days out depending on priority.
Arrears are computed from months elapsed since the last payment versus payments
made; escalation applies a CPI floor of 4.5% for `cpi_linked` leases and a +5%
premium for `market_review` leases.

### 4.5 Materials REST routes (`routes/materials.ts`, anchor 58.1.3.5)

The materials routes manage the catalog, warehouse inventory, and production QC
lifecycle. Material SKUs and batch numbers are unique across their tables; the
route returns HTTP 409 on a duplicate. The low-stock alert endpoint and QC
summary dashboard aggregate across all materials, so they use a separate
`/inventory/alerts` and `/batches/qc-summary` path rather than nesting under a
material ID.

All routes live under `/api/v1/materials`:

| Method | Path                       | Permission        | Purpose                                         |
| ------ | -------------------------- | ----------------- | ----------------------------------------------- |
| GET    | `/`                        | `materials:read`  | List material catalogue                         |
| POST   | `/`                        | `materials:write` | Add a material (unique SKU; 409 on conflict)    |
| GET    | `/:id`                     | `materials:read`  | Get material (by id or SKU)                     |
| GET    | `/:id/inventory`           | `materials:read`  | Stock levels by location                        |
| PUT    | `/:id/inventory/:location` | `materials:write` | Update stock at a location                      |
| GET    | `/:id/batches`             | `materials:read`  | List production batches                         |
| POST   | `/:id/batches`             | `materials:write` | Create a production batch (unique batch number) |
| GET    | `/:id/batches/:batchId/qc` | `materials:read`  | QC records for a batch                          |
| POST   | `/:id/batches/:batchId/qc` | `materials:write` | Submit a QC test result                         |
| GET    | `/inventory/alerts`        | `materials:read`  | Low-stock alerts across all materials           |
| GET    | `/batches/qc-summary`      | `materials:read`  | QC pass-rate dashboard                          |

`CreateMaterialSchema` requires `sku` (uppercase alphanumeric, regex
`^[A-Z0-9-]+$`), `category` (one of the 11 `cybele_mat_category` values),
`unitOfMeasure`, `unitCostGHS` (positive), `leadTimeDays` (0–365, default 7).
`QCTestSchema.testType` is one of 15 test types; `assessQCStatus` returns
`Pending` when neither `minAcceptable` nor `maxAcceptable` is set, `Failed` when
the result is outside the band, otherwise `Passed`. A batch's `qcStatus` is
`Failed` if any test failed, `Pending` if there are no tests or any test is
pending, otherwise `Passed`. Batch cost is split into raw material / labour /
overhead with per-component percentages.

### 4.6 Authentication and RBAC (`auth.ts`, anchor 58.1.3.9)

Cybele uses HS256 JWTs verified in-process. The token carries the caller's role,
and the `ROLE_PERMISSIONS` matrix maps each role to its allowed operations. The
`requireOwnership` guard adds a resource-level check on top of role-level
permission for `owner`-role callers. Below are the full token payload shape,
role list, permission list, and role-to-permission matrix.

Cybele uses HS256 JWTs verified in-process (`verifyJwt`) — header `alg` must be
`HS256`, the signature is checked with `timingSafeEqual`, the token must not be
expired, and the issuer must be `cybele-auth`.

- **`CybeleTokenPayload`** — `sub` (user/service account ID), `role`,
  `ownerId`/`tenantId`/`agentId` (role-specific scoping), `exp`, `iat`, `iss`.
- **`CybeleRole`** — `owner`, `tenant`, `agent`, `contractor`, `developer`,
  `finance`, `admin`.
- **`Permission`** — `property:read`/`write`/`delete`, `lease:read`/`write`,
  `maintenance:read`/`write`, `construction:read`/`write`,
  `materials:read`/`write`, `finance:read`/`write`, `listing:read`/`write`,
  `market-intel:read`, `admin:*`.
- **`ROLE_PERMISSIONS`** — explicit grant matrix. `owner`: property read/write,
  lease read/write, maintenance read/write, listing read/write, finance read,
  market-intel read. `tenant`: property read, lease read, maintenance
  read/write, listing read. `agent`: property read, listing read/write,
  market-intel read, maintenance read. `contractor`: construction read/write,
  materials read, property read. `developer`: property read/write, construction
  read/write, materials read/write, finance read, market-intel read. `finance`:
  property read, finance read/write, market-intel read, construction read.
  `admin`: `admin:*` (grants everything).
- `requirePermission(permission)` throws 401 if unauthenticated, 403 if the role
  lacks the permission. `requireOwnership(getResourceOwnerId)` ensures an
  `owner`-role caller can only touch resources where `ownerId` matches; `admin`
  bypasses, other roles are not subject to the check.

### 4.7 Rate limiting (`rate-limit.ts`, anchor 58.1.3.10)

Rate limits are tiered by role, with per-endpoint overrides for routes that are
known to be expensive. The sliding-window counter uses Redis sorted sets and a
60-second window.

Sliding-window counter backed by Redis sorted sets. Per-role tiers in
`RATE_LIMIT_TIERS` (`maxRequests` per 60000 ms): `anonymous` 60, `owner` 200,
`tenant` 100, `agent` 500, `contractor` 200, `developer` 300, `finance` 200,
`admin` 2000. `ENDPOINT_RATE_LIMITS` adds stricter per-endpoint overrides:
`GET /api/v1/properties` 30/min (expensive PostGIS queries),
`POST /api/v1/listings` 10/min (anti-spam), `POST /api/v1/finance` 20/min
(CPU-bound), and a market-intel export override.

### 4.8 GraphQL API (`graphql/schema.ts`, anchor 58.1.3.6)

The GraphQL API complements the REST surface for queries that span multiple
aggregates or need flexible spatial filtering. It is particularly used by the
marketplace app for listing search and by the portfolio app for cross-asset
financial views.

`@cybele/api` exports a schema-first GraphQL SDL (`typeDefs`) and a typed
resolver map (`resolvers`, with `ResolverContext` and `Resolvers` types). The
SDL declares `DateTime` and `JSON` scalars; enums mirror the core enums
(`PropertyType`, `PropertyStatus`, `ConstructionStatus`, `GhanaRegion`,
`MaterialCategory`, `TransactionType`). Key object types: `Property`, `Listing`,
`Transaction` (comparable sale/rental), `ConstructionProject`, `Lease`,
`Tenant`, `MarketPrice`/`PriceIndex`, plus `SpatialFilter` input supporting
radius (centre + km), polygon (GeoJSON coordinates), district, and region
filtering.

`Query` fields: `property`, `properties` (with `PropertyFilter` + pagination),
`listing`, `listings`, `listingsNearby` (lat/lon/radiusKm, distance-ordered),
`marketTransactions`, `priceIndex`, `constructionProject`,
`constructionProjects`, `tenant`, `tenants`, `lease`, `leases`, `reitFund`,
`reitFunds`, `material`, `materials`. `Mutation` fields include
`createProperty`, `updateProperty`, `createListing`, `updateListingStatus`,
`createLease`, `renewLease`, `terminateLease`.

### 4.9 gRPC service definitions (`grpc/definitions.ts`, anchor 58.1.3.8)

gRPC services expose Cybele's core operations as strongly-typed RPC interfaces,
suitable for service-to-service calls from other Oshun domains. Streaming RPCs
(`SearchPropertiesNearby`, `StreamProjectUpdates`, `StreamPaymentEvents`,
`SendBulk`) enable real-time data push without polling.

Five Protocol Buffers 3 service definitions are embedded as TypeScript string
constants and registered in `CYBELE_GRPC_SERVICES` with default ports:

| Service               | Package                  | Port  | RPCs                                                                                                                                                       |
| --------------------- | ------------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PropertyService`     | `cybele.property.v1`     | 50051 | `GetProperty`, `ListProperties`, `CreateProperty`, `UpdateProperty`, `DeleteProperty`, `ComputeValuation`, `SearchPropertiesNearby` (bidirectional stream) |
| `ConstructionService` | `cybele.construction.v1` | 50052 | `GetProject`, `UpdateProgress`, `ComputeEarnedValue`, `StreamProjectUpdates` (server stream)                                                               |
| `LeaseService`        | `cybele.lease.v1`        | 50053 | `GetLease`, `RecordPayment`, `GetArrears`, `StreamPaymentEvents` (server stream)                                                                           |
| `FinanceService`      | `cybele.finance.v1`      | 50054 | `AssessMortgage`, `ComputeReitNav`                                                                                                                         |
| `NotificationService` | `cybele.notification.v1` | 50055 | `Send`, `SendBulk` (bidirectional stream)                                                                                                                  |

`NotificationService` defines `NotificationChannel` (`SMS`, `WHATSAPP`, `EMAIL`,
`PUSH`, `IN_APP`) and `NotificationPriority` (`NORMAL`, `HIGH`, `CRITICAL`).

### 4.10 WebSocket server (`websocket/server.ts`, anchor 58.1.3.7)

The WebSocket server delivers real-time push updates to connected clients.
Clients subscribe to typed channels (e.g. `project:proj-123`) after
authenticating with a JWT on the upgrade request. Channel subscription is
authorized by the same role-based rules as the REST API.

`CybeleWebSocketServer` delivers real-time updates over `ws`. Clients
authenticate by passing a JWT in a `?token=` query parameter (or
`Authorization: Bearer` header) on the upgrade. Channels follow the format
`<type>:<id>` with types `project`, `lease`, `property`, `building`, `agent`,
`iot`.

- **`WsMessageType`** — `progress_update`, `milestone_reached`,
  `inspection_submitted`, `variation_order`, `rent_received`,
  `rent_arrears_alert`, `maintenance_raised`, `maintenance_resolved`,
  `listing_enquiry`, `price_alert`, `iot_sensor_reading`, `iot_sensor_alarm`,
  `building_occupancy`, `ping`, `pong`, `subscribe`, `unsubscribe`, `error`.
- **`WsMessage`** — `type`, `channel`, `data`, `timestamp`, `correlationId`
  (optional).
- `canSubscribeToChannel(channel, userRole, userId)` authorizes subscriptions
  per channel type: `project` channels are open to
  `contractor`/`developer`/`owner`/`admin`; `lease` to `tenant`/`owner`/`admin`;
  `property` to `owner`/`agent`/`contractor`/`developer`/`admin`; `building` and
  `iot` to `owner`/`developer`/`admin`; `agent` to `agent`/`admin`. `admin` may
  subscribe to anything. `broadcast(channel, message)` fans a message out to
  every subscribed client.

### 4.11 Application gateway (`apps/cybele/api`, anchor 58.17.5.x)

The deployed API gateway app (distinct from the `@cybele/api` library) adds
operational management endpoints: routing configuration, client token lifecycle,
rate-limit policy, audit log access, downstream service health, OpenAPI
documentation, and webhook delivery. These management endpoints are not exposed
by the library itself — they belong to the deployed application boundary.

Source: `apps/cybele/api/src/routes/gateway.ts`. The deployable API app exposes
gateway-management endpoints under its own router:

- `GET /routes` — routing table (`/api/v1/projects/**`, `/portfolio/**`,
  `/marketplace/**`, `/factory/**`).
- `POST /auth/tokens`, `POST /auth/tokens/:clientId/revoke`,
  `GET /auth/tokens/:clientId` — client token issue/revoke/inspect.
- `GET /rate-limits`, `POST /rate-limits`, `POST /rate-limits/check` —
  per-client rate-limit configuration and check.
- `GET /versions` — API version manifest (current `v1`, URL-path versioning).
- `GET /logs`, `POST /logs` — audit-log query and append (`AuditLogEntry` with
  `clientId`, `method`, `path`, `statusCode`, `correlationId`, `durationMs`; the
  store keeps the last 10000 entries).
- `GET /health/services`, `POST /health/services/:serviceId/ping`,
  `POST /services` — service-registry health aggregation and registration
  (built-in services: `cybele-projects`, `cybele-portfolio`,
  `cybele-marketplace`, `cybele-factory`).
- `GET /openapi.json` — OpenAPI 3.1.0 document for the projects, portfolio,
  marketplace, factory, and gateway APIs.
- `GET /webhooks`, `POST /webhooks`, `PATCH /webhooks/:webhookId`,
  `DELETE /webhooks/:webhookId`, `POST /webhooks/:webhookId/deliver`,
  `GET /webhooks/:webhookId/deliveries` — webhook subscription and delivery
  management. A `WebhookSubscription` carries `targetUrl`, an `events` array, an
  HMAC signing `secret`, `isActive`, and a `failureCount`; `WebhookDelivery`
  records `event`, `payload`, `status` (`pending` | `success` | `failed` |
  `retrying`), and `attempts`.

### 4.12 Application service domains

The four non-gateway apps each define a small in-memory domain layer (their own
state machine types and in-memory stores) that sits above the library packages.
This means each app can be deployed and tested independently without requiring a
live database, while the library packages provide the domain logic. The key
state machines per service are listed below:

- **`apps/cybele/projects`** — `ProjectStage` lifecycle (`feasibility` →
  `planning` → `design` → `construction` → `commissioning` → `handover` →
  `complete`); endpoints for dashboard, schedule/Gantt, daily reports, RFIs,
  change orders, QC, safety, earned value, documents, notifications.
- **`apps/cybele/portfolio`** — `Property` (status `active` |
  `under_development` | `vacant` | `disposed`), `Lease` (status `active` |
  `notice_given` | `expired` | `terminated`), `RentRecord`; endpoints for
  portfolio summary, property detail, tenants, rent collection, maintenance,
  financials, valuation, lease calendar.
- **`apps/cybele/marketplace`** — `Listing` (status `active` | `under_offer` |
  `sold` | `let` | `withdrawn`), `Agent`; endpoints for listings, search,
  recommendations, agents, inquiries/leads, viewings, comparison, market
  analytics, favourites.
- **`apps/cybele/factory`** — `ProductionStation` pipeline (`scheduled` →
  `framing` → `rebar` → `concrete_pour` → `curing` → `MEP_rough` → `insulation`
  → `board_up` → `MEP_finish` → `finishing` → `QC_inspection` →
  `dispatch_ready`), `ProductionOrder` (status `draft` | `confirmed` |
  `in_production` | `qc_hold` | `ready` | `dispatched` | `cancelled`); endpoints
  for production orders, station scheduling, QC, material inventory, logistics,
  KPI dashboard, module configuration.

---

## 5. Contract Schemas

Source: `libs/contracts/cybele/src/api-schemas.ts`. These Zod schemas are the
stable inter-domain contract surface and are independent of the runtime route
schemas in `@cybele/api`.

The distinction matters: the runtime route schemas in `@cybele/api` can evolve
with the service implementation; the contract schemas in `@contracts/cybele` are
the external promises Cybele makes to other domains. Other domains import only
from `@contracts/cybele` — never from `@cybele/api` or `@cybele/core` directly.
Each schema pair below defines what is acceptable for creating a record
(request) and what is guaranteed to be present in a response.

- **`CreatePropertyRequestSchema` / `PropertyResponseSchema`** — `type` (six
  literals; note this contract omits `Healthcare`/`Education`), `status` (six
  literals), `address` (with the `^[A-Z]{2}-\d{3}-\d{4}$` digital-address
  regex), `location` (latitude 4–12, longitude −4–2), `grossArea` (positive),
  `description` (20–2000 chars), `amenities`, `images` (URLs). The response
  extends the request with `id`, `currentValue`, `createdAt`, `updatedAt`.
- **`CreateProjectRequestSchema` / `ProjectResponseSchema`** — `name` (3–200
  chars), `type` (`residential` | `commercial` | `industrial` | `infrastructure`
  | `mixed_use`), `propertyId` (optional), `contractValueGHS` (positive),
  `startDate`/`plannedCompletionDate`, `projectManager`, `clientId`. The
  response adds `id`, `status` (`draft` | `active` | `on_hold` | `completed` |
  `cancelled`), `progressPct` (0–100), `actualCostGHS`, `createdAt`,
  `updatedAt`.
- **`CreateLeaseRequestSchema` / `LeaseResponseSchema`** — `unitId`, `tenantId`,
  `type` (six `LeaseType` literals), `startDate`/`endDate` (refined: end after
  start), `rentAmountGHS` (positive), `depositAmountGHS`, `escalationType`,
  `escalationRate` (0–50%). The response adds `id`, `status` (`draft` | `active`
  | `expired` | `terminated` | `renewed`), `nextRentDue`, `totalArrearsGHS`,
  `createdAt`, `updatedAt`.
- **`MortgageApplicationSchema` / `MortgageDecisionSchema`** — `applicantId`,
  `propertyId`, `loanAmountGHS` (positive), `tenureMonths` (12–360),
  `productId`, `employmentType` (`salaried` | `self_employed` |
  `business_owner`), `grossMonthlyIncomeGHS`, `monthlyDebtObligationsGHS`,
  `creditScore` (300–850). The decision carries `status` (`approved` |
  `conditional` | `declined` | `pending_docs`), `approvedAmountGHS`,
  `interestRatePct`, `monthlyInstallmentGHS`, `conditions`, `declinedReasons`,
  `debtToIncomeRatio`, `decidedAt`.
- **`CreateListingRequestSchema` / `ListingResponseSchema`** — `title` (10–200
  chars), `description` (50–5000 chars), `propertyType` (`house` | `apartment` |
  `office` | `land` | `warehouse` | `retail` | `hotel`), `transactionType`
  (`sale` | `rent` | `lease`), `priceGHS`, `bedrooms`/`bathrooms` (0–20),
  `areaSqm`, `district`, `region`, `amenities`, `images` (≥1 required). The
  response adds `id`, `status` (`active` | `pending` | `sold` | `withdrawn` |
  `expired`), `viewCount`, `enquiryCount`, `publishedAt`, `createdAt`,
  `updatedAt`.
- **`PropertyValuationRequestSchema` / `PropertyValuationResponseSchema`** —
  `propertyId`, `valuationType` (`market_value` | `forced_sale` |
  `insurance_replacement` | `rental_value`), `purposeOfValuation` (`mortgage` |
  `sale` | `taxation` | `insurance` | `accounting`), `valuerId`,
  `inspectionDate`. The response carries `estimatedValueGHS`, `confidenceLevel`
  (`high` | `medium` | `low`), `comparableTransactions`, `valuationDate`,
  `validUntil`.

---

## 6. Domain Events

Cybele publishes domain events as CloudEvents 1.0 envelopes over Apache Kafka.
There are two event-definition surfaces and one runtime publisher.

Understanding the two surfaces is important: the contract registry in
`@contracts/cybele` defines logical event types and the CloudEvents envelope
schema — this is the stable external contract. The runtime registry in
`@cybele/api/src/kafka.ts` defines physical Kafka topic names, partition counts,
and the publisher/consumer implementation. Other domains subscribe to the
logical contract events; the physical topic names are an implementation detail.

### 6.1 Contract event registry (`@contracts/cybele`)

Source: `libs/contracts/cybele/src/events.ts`.

- **`CybeleEventSchema`** — the CloudEvents 1.0 envelope (Zod): `specversion`
  (literal `1.0`), `id`, `source`, `type`, `subject`, `time` (datetime),
  `datacontenttype` (literal `application/json`), `data` (unknown),
  `correlationId` (optional), `businessUnit`.
- **`CYBELE_EVENT_TYPES`** — the canonical event-type registry, all named
  `cybele.<aggregate>.<event>`:
  - Property: `cybele.property.created`, `cybele.property.updated`,
    `cybele.property.listed`, `cybele.property.sold`,
    `cybele.property.valuation_done`.
  - Construction: `cybele.project.created`, `cybele.project.milestone_reached`,
    `cybele.project.completed`, `cybele.project.quality_inspection_failed`,
    `cybele.project.safety_incident`, `cybele.project.variation_order`,
    `cybele.project.progress_claim`.
  - Lease: `cybele.lease.executed`, `cybele.lease.renewed`,
    `cybele.lease.terminated`, `cybele.lease.rent_received`,
    `cybele.lease.arrears_warning`, `cybele.lease.rent_review`.
  - Finance: `cybele.finance.mortgage_approved`,
    `cybele.finance.mortgage_declined`, `cybele.finance.capital_call`,
    `cybele.finance.distribution`, `cybele.finance.payment_default`.
  - Prefab: `cybele.prefab.order_created`, `cybele.prefab.module_completed`,
    `cybele.prefab.module_delivered`, `cybele.prefab.module_installed`.
  - Market: `cybele.market.listing_created`, `cybele.market.listing_expired`,
    `cybele.market.offer_received`, `cybele.market.offer_accepted`.
- **`CYBELE_KAFKA_TOPICS`** (contracts) — `cybele.properties`,
  `cybele.construction`, `cybele.leases`, `cybele.finance`, `cybele.prefab`,
  `cybele.market`. `resolveKafkaTopic(eventType)` routes a CloudEvents type to
  its topic by aggregate prefix.
- **Event payload types** — `PropertyCreatedData`, `ProjectMilestoneData`,
  `SafetyIncidentData`, `LeaseExecutedData`, `RentPaymentData`,
  `RentArrearsData`, `MortgageApprovedData`, `CapitalCallData`,
  `DistributionData`, `PrefabModuleData`, `ListingCreatedData`. For example,
  `ProjectMilestoneData` carries `projectId`, `milestoneName`, `completionPct`,
  `plannedDate`, `actualDate`, `varianceDays`; `RentArrearsData` carries
  `escalationLevel` (`warning` | `notice` | `legal`).
- **`buildCybeleEvent(type, subject, data, businessUnit, correlationId?)`**
  constructs an envelope; **`validateCybeleEvent(event)`** safe-parses it
  against `CybeleEventSchema`.

### 6.2 Runtime Kafka integration (`@cybele/api`)

Source: `libs/cybele/api/src/kafka.ts` (anchor 58.1.3.18). The runtime
publisher/consumer use `kafkajs` and a separate physical-topic registry:

- **`CYBELE_KAFKA_TOPICS`** (runtime) — `cybele-property-events`,
  `cybele-construction-events`, `cybele-lease-events`, `cybele-finance-events`,
  `cybele-iot-events`, `cybele-notifications`, `cybele-audit-events`.
- **`TOPIC_PARTITION_COUNTS`** — recommended partition counts:
  `cybele-property-events` 12, `cybele-construction-events` 6,
  `cybele-lease-events` 6, `cybele-finance-events` 3, `cybele-iot-events` 24
  (high-volume), `cybele-notifications` 3, `cybele-audit-events` 6. Partition
  key is `propertyId`/`tenantId`/`projectId` so every event for a resource lands
  on the same partition, preserving per-resource ordering.
- **`CybeleEventPublisher`** — `connect`, `disconnect`, `publish`,
  `publishBatch`; messages are GZIP-compressed and carry `ce-*` CloudEvents
  headers.
- **`CybeleEventConsumer`** — registers per-event-type handlers via `on()`,
  consumes, and routes failures to a dead-letter topic or handler
  (`KafkaDeadLetterRecord`, `createDeadLetterRecord`).
- `createCybeleEvent(...)` builds the CloudEvents envelope;
  `createDefaultPublisher()` reads brokers from `KAFKA_BROKERS`.

The runtime publisher also exports Prometheus counters `kafkaMessagesPublished`
and `kafkaMessagesConsumed`.

---

## 7. Construction On-Chain Ledger

Source: `libs/cybele/api/src/routes/construction-ledger.ts`. The Earned Value
Management endpoint (`GET /construction/:id/earned-value`) does not trust
ephemeral route state — it derives its inputs from an authoritative,
hash-chained, signed ledger.

The ledger exists because construction finance depends on tamper-evident
records: lenders and investors need to know that a budget commitment, a progress
attestation, or a payment settlement has not been retroactively altered. The
hash-chain design (each event commits to the previous event's hash) means any
modification to a past event changes every subsequent hash and is detected by
`verifyChain`. The default `InMemoryConstructionLedger` is suitable for testing;
production deployments swap in an EVM/JSON-RPC backend via
`setConstructionLedger(...)`.

### 7.1 Ledger events

Each ledger event corresponds to one EVM-relevant construction mutation. The
four event kinds capture the four moments that matter to project finance:
committing a budget, attesting progress, settling a payment, and achieving a
milestone. The hash and signature fields make each event self-authenticating.

**`ConstructionEventKind`** — `budget_committed`, `progress_attested`,
`payment_settled`, `milestone_achieved`. Each event extends `OnChainEventBase`:
`kind`, `projectId`, `recordedAt`, `transactionHash` (`0x` + SHA-256 of
canonical event data), `blockNumber` (monotonically increasing), `prevHash`
(previous on-chain hash for this project, forming a per-project Merkle-style
chain — `0x000…0` at genesis), `signature` (ECDSA secp256k1 over the canonical
data), `validatorAddress`.

- **`BudgetCommittedEvent`** — `budgetAtCompletionGHS`, `startDate`,
  `plannedCompletionDate`, `contractRef`.
- **`ProgressAttestedEvent`** — `percentComplete`, `attestedBy`, `attestedAt`.
- **`PaymentSettledEvent`** — `claimId`, `amountGHS`, `paidAt`, `payerAddress`,
  `payeeAddress`.
- **`MilestoneAchievedEvent`** — `phaseId` (optional), `milestoneId`,
  `achievedAt`, `linkedPaymentGHS` (optional).

`ConfirmedConstructionEvent` projects an event with a `confirmations` count
(`currentBlock − blockNumber + 1`).

### 7.2 Ledger contract and backend

`ConstructionLedger` defines `recordBudget`, `recordProgress`, `recordPayment`,
`recordMilestone`, `getProjectEvents`, `getCurrentBlock`, `verifyChain`
(validates the per-project hash chain and every signature, returning the failing
index and reason on a break), and `reset`. The default
`InMemoryConstructionLedger` keeps the chain in process memory with a
deterministic signing secret and a genesis block (`12_000_000`).
`setConstructionLedger(...)` swaps in a real EVM/JSON-RPC backend. The route
layer wires ledger writes to the four EVM-relevant mutations (create-project →
`budget_committed`, update-progress → `progress_attested`, progress-claim →
`payment_settled`, phase-complete → `milestone_achieved`).

### 7.3 EVM derivation

Earned Value Management requires four input scalars: Budget at Completion (BAC),
Planned Value (PV), Earned Value (EV), and Actual Cost (AC). Cybele derives
these from the authoritative ledger — not from the database state — so the EVM
computation is reproducible and auditable at any point in time.

`deriveEvmInputs(...)` aggregates confirmed ledger events into the four EVM
scalars: `budgetAtCompletion` (most recent confirmed `budget_committed`; falls
back to the in-memory contract value, marked `source: 'in_memory_fallback'`),
`percentComplete` (latest confirmed `progress_attested`, else derived from
achieved-milestone count), `actualCostToDate` (sum of confirmed
`payment_settled.amountGHS` with `paidAt ≤ asOf`), `plannedValueToDate` (linear
S-curve of `BAC × elapsed-fraction`). `computeEarnedValue(...)` then produces
the standard EVM metrics: earned value, schedule variance, cost variance, SPI,
CPI, TCPI, EAC (estimate at completion), ETC, VAC (variance at completion), and
`scheduleStatus`/`costStatus` classifications.

---

## 8. Cross-Domain Integration

Source: `libs/cybele/integration/src/*` (`brigid-bridges`, `saraswati-bridges`,
`asase-bridges`, `freya-bridges`, `maat-bridges`), re-exported from `index.ts`.
All exchange flows through `@cybele/integration` and `@contracts/cybele`;
consumers never import Cybele internals.

Each bridge is a dedicated adapter module responsible for translating between
Cybele's internal types and the stable contract types used by the peer domain.
This means a change to Cybele's internal schema only requires updating the
bridge, not the consuming domain. Cross-domain endpoint URLs are
environment-driven (see §9) so the same bridge code works in every deployment
environment.

- **Brigid** — supplies industrial automation, energy, maintenance, and
  materials/manufacturing intelligence for Cybele factories and facilities.
- **Saraswati** — supplies advanced-technology manufacturing, energy devices,
  telecom, IoT, and security-systems intelligence.
- **Asase** — consumes Cybele infrastructure and site-planning data for
  agricultural facilities.
- **Freya** — consumes Cybele retail, manufacturing, and real-estate
  intelligence for luxury stores, workshops, and factories.
- **Maat** — consumes Cybele financials, risk, and capital-planning data.

Cross-domain endpoint URLs are environment-driven (see §9): `BRIGID_API_URL`,
`SARASWATI_API_URL`, `ASASE_API_URL`, `FREYA_API_URL`, `MAAT_API_URL`.

Phase 175 additionally adds Gaia storm, cyclone, precipitation, heat, wind,
urban-downscaling, and climate-scenario products as inputs to Cybele (pre-storm
asset-hardening alerts, construction-site weather risk, drainage and flood
planning, urban heat-island analysis, climate adaptation, schedule risk, and
portfolio resilience). The `WeatherCondition` core type (§2.14) is the on-site
weather record those products enrich.

---

## 9. Configuration and Environment

Source: `libs/cybele/README.md` and the gateway/Kafka modules.

All Cybele services read configuration from environment variables at startup.
The table below lists every variable, its purpose, and the default value where
one is defined in code. The local development stack is started with
`docker compose -f docker/docker-compose.cybele.yml up -d`.

| Variable                                                                                    | Purpose                                                                       |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `CYBELE_DATABASE_URL`                                                                       | PostgreSQL connection string (`?schema=cybele`)                               |
| `REDIS_URL`                                                                                 | Redis URL for caching and rate-limit state (default `redis://localhost:6379`) |
| `KAFKA_BROKERS`                                                                             | Comma-separated Kafka broker list (default `localhost:9092`)                  |
| `JWT_SECRET`                                                                                | HS256 secret for verifying access tokens                                      |
| `CYBELE_DOCUMENT_BUCKET`                                                                    | Object-storage bucket for documents                                           |
| `CYBELE_DRAWINGS_BUCKET`                                                                    | Object-storage bucket for drawings/BIM files                                  |
| `CYBELE_SITE_PHOTOS_BUCKET`                                                                 | Object-storage bucket for site photos                                         |
| `GHANA_LANDS_COMMISSION_API_URL`                                                            | Ghana Lands Commission API base URL                                           |
| `GHIPSS_API_URL`                                                                            | Ghana Interbank Payment and Settlement Systems API                            |
| `MOBILE_MONEY_API_URL`                                                                      | Mobile-money payment API                                                      |
| `BRIGID_API_URL` / `SARASWATI_API_URL` / `ASASE_API_URL` / `FREYA_API_URL` / `MAAT_API_URL` | Cross-domain service endpoints                                                |

The local infrastructure stack is started with
`docker compose -f docker/docker-compose.cybele.yml up -d`.

---

## 10. Invariants and Hard Requirements

These constraints are enforced in the code cited above and apply across the
entire domain. They are not optional — violating any of them would produce
incorrect financial results, invalid geospatial data, or compromised audit
trails. Engineers adding new routes, schemas, or migrations must verify that
these invariants are preserved.

- **Ghana geographic bounds** — property and listing `location` inputs are
  validated to Ghana's bounding box: latitude 4–12°N, longitude −4–2°E
  (`CreatePropertySchema`, `CreatePropertyRequestSchema`).
- **Digital-address format** — `digitalAddressGps` must match
  `^[A-Z]{2}-\d{3}-\d{4}$` (Ghana Post GPS), enforced in `core` validators and
  route schemas.
- **Coordinate reference integrity** — `GeoLocation`/`GeoPolygon` carry an
  explicit `srid` (default 4326); the PostGIS `boundary_geometry` column is
  declared `geometry(Polygon,4326)`. Spatial data is never stored or computed
  without its CRS.
- **Lease date ordering** — `endDate` must be strictly after `startDate`
  (`CreateLeaseRequestSchema` and `CreateLeaseSchema` refinements); construction
  `plannedCompletionDate` must be after `startDate`, and a phase's `plannedEnd`
  after its `plannedStart`.
- **Escalation bounds** — lease `escalationRate` is constrained to 0–50% per
  annum.
- **Mortgage bounds** — `tenureMonths` 12–360, `creditScore` 300–850;
  underwriting tracks `debtToIncomeRatio` against the Bank of Ghana ≤ 43%
  guideline.
- **Auditable construction changes** — every EVM-relevant change appends a
  signed, hash-chained event to the construction ledger; `verifyChain` detects
  any tampering by reporting the first failing event index and reason.
- **REIT NAV identity** — `nav_ghs` is gross asset value minus total
  liabilities; `nav_per_unit_ghs` is NAV divided by units outstanding;
  `REITFund.sectorAllocation` percentages must sum to 100.
- **Material SKU and batch uniqueness** — material SKUs and batch numbers are
  unique; the routes return HTTP 409 on a duplicate, and `sku`/`batch`
  identifiers are uppercase-alphanumeric.
- **Per-resource event ordering** — Kafka events are partitioned by resource ID
  so all events for a property/tenant/project preserve ordering within their
  partition.
- **Soft deletion** — property deletion is a soft delete (`deletedAt` is set);
  records are retained for historical reporting.

---

## 11. Verification Expectations

Changes run the affected packages' lint, type-check, and test gates
(`pnpm nx test|lint|build <project>`, or, when Nx is unavailable,
`npx vitest run` and `npx tsc --noEmit` from the package directory).

The `@cybele/testing` package provides the fixtures for domain correctness tests
— not just shape tests. Geospatial, financial, lease, construction-schedule, and
compliance changes are in a higher risk tier and require explicit regression
fixtures that would fail on incorrect computations. Geospatial, financial,
lease, construction-schedule, and compliance changes additionally require
explicit fixtures and regression tests; `@cybele/testing` provides the fixtures
and mocks those tests build on. Each library ships co-located test files
(`*.test.ts`, `*.spec.ts`, integration tests) covering domain correctness —
including the construction ledger's hash-chain and signature verification, the
EVM derivation, the GLC integration types, and the materials QC pass/fail logic.

---

## 12. Grounding Statement

Every package name in §1 is taken from the corresponding `package.json`. Every
type, enum, and field in §2 is defined in `libs/cybele/core/src/types.ts`
(validators/guards in `validators.ts`/`guards.ts`). Every table, column, and
PostgreSQL enum in §3 is defined in `libs/cybele/db/src/schema.ts`; pooling and
Redis facts come from `connection.ts` and `redis-config.ts`. Every endpoint,
middleware, and RBAC rule in §4 is defined in `libs/cybele/api/src/gateway.ts`,
`src/auth.ts`, `src/rate-limit.ts`, the four `src/routes/*.ts` files,
`src/graphql/schema.ts`, `src/grpc/definitions.ts`, `src/websocket/server.ts`,
and `apps/cybele/api/src/routes/gateway.ts`; the app-service domains come from
each `apps/cybele/*/src/routes/*.ts`. Every contract schema in §5 is defined in
`libs/contracts/cybele/src/api-schemas.ts`. Every event, topic, and payload type
in §6 is defined in `libs/contracts/cybele/src/events.ts` and
`libs/cybele/api/src/kafka.ts`. The ledger in §7 is defined in
`libs/cybele/api/src/routes/construction-ledger.ts`. The integration bridges in
§8 are defined under `libs/cybele/integration/src/`. The environment variables
in §9 come from `libs/cybele/README.md`. No schema, enum, endpoint, event,
table, or state machine in this document was invented; anything not present in
the cited code is not described here.
