Domain · Specifications

Cybele Domain — Technical Specifications

Cybele's logic is split into 19 focused library packages rather than one monolithic module.

12sections54 minread

On this page

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 enums (string-valued):

  • PropertyStatusPlanning, UnderConstruction, Completed, ForSale, ForRent, Occupied, UnderRenovation, Demolished.
  • ConstructionStatusBidding, Awarded, Mobilization, InProgress, Substantial (substantial completion), Defects (defects liability period), FinalCompletion.
  • LeaseTypeFixedTerm, MonthToMonth, RentToOwn, GroundLease, TripleNet, GrossLease.
  • PropertyTypeResidential, Commercial, Industrial, MixedUse, Land, Hospitality, Healthcare, Education.
  • ValuationMethodComparative, Income, Cost, DCF.
  • ProjectPhaseTypePreconstruction, 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).
  • ZoningClassResidential_Low, Residential_Medium, Residential_High, Commercial, Industrial_Light, Industrial_Heavy, Mixed_Use, Agricultural, Conservation, Institutional.
  • LandTenureFreehold, 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.

  • GhanaAddressregion (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.
  • GeoLocationlatitude, longitude, elevation (metres above sea level), srid (Spatial Reference ID; default WGS-84 = 4326).
  • GeoPolygoncoordinates (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.

  • PropertyValuationid, propertyId, method (ValuationMethod), value (GHS), currency, date, valuer (registered valuer/firm), confidence (0–1 coefficient), assumptions (string list), marketComparables (optional MarketComparable[]).
  • MarketComparableaddress, saleDate, salePrice, area, pricePerSqm, adjustmentFactor, adjustedPrice.
  • ZoningClassificationclass (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.

LandTitleid (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 Units 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.

  • Plotid (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).
  • Buildingid (BuildingId), propertyId, name, floors, totalUnits, grossArea, netLeasableArea, structuralSystem (rc_frame | steel_frame | load_bearing | timber), yearBuilt, condition (excellent | good | fair | poor), hasLift, hasBackupPower, hasBorehole.
  • Unitid (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.

  • Milestoneid, name, plannedDate, actualDate (Date | null), isPredecessorComplete, completionCriteria, isCriticalPath, linkedPayment (GHS unlocked by this milestone, or null for a non-payment milestone).
  • ProjectPhaseid, name (ProjectPhaseType), status (ConstructionStatus), plannedStart, plannedEnd, actualStart, actualEnd (nullable), percentComplete, cost (GHS), milestones (Milestone[]).
  • ConstructionProjectid (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.

  • Contractorid (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.

  • Tenantid (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.
  • Leaseid (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.
  • RentScheduleleaseId, 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.

  • MortgageProductid, name, rateType (fixed | variable | hybrid), nominalRate, effectiveRate (annual %, fees/compounding included), maxLTV (max loan-to-value %), minTenureMonths, maxTenureMonths, minLoanAmount, maxLoanAmount, eligibilityCriteria.
  • AmortizationEntryperiod, paymentDate, openingBalance, payment, principal, interest, closingBalance.
  • REITFundid (FundId), name, ticker, nav (GHS), navPerUnit, unitsOutstanding, distributionYield (%), totalAssets, totalLiabilities, sectorAllocation (Record<string, number>, values must sum to 100), inceptionDate.
  • DevelopmentProFormaprojectId, 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.

  • PrefabModuleid, 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.
  • ConcreteBlockdimensions (150x225x450mm | 200x225x450mm | 100x225x450mm), strengthNmm2 (compressive, N/mm²), density (kg/m³), hasHollow, gsCertified (Ghana Standards Authority), batchNumber, testDate, testResult (pass | fail).
  • PaintProductid, formulationCode, name, color, hexCode, finish (matte | eggshell | satin | semi_gloss | gloss), vocGPerL (volatile organic compounds, g/L), coverageM2PerL, dryingTimeHours, shelfLifeMonths.
  • SteelProductid, 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.
  • BuildingMaterialid (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:

  • IndustrialParkid, name, location, totalArea (hectares), developedArea, plots, vacantPlots, occupancyRate (%), isFreeZone (GIPC Free Zone status), anchorTenants, utilities (array of power | water | sewer | fibre | gas), masterPlanUrl.
  • DataCenterid, name, tierLevel (Uptime Institute Tier 1–4), powerCapacityMW, pueTarget (Power Usage Effectiveness), rackCount, totalFloorArea, colocationAvailable (m²), connectivity, certifications.
  • HospitalityPropertyid, 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 (%).
  • StudentHousingid, propertyId, university, bedCount, distanceToMainGate (km), amenities, rentPerBedPerAnnum (GHS), occupancyRate (%), mealsIncluded.
  • RetailPropertyid, propertyId, gla (gross leasable area, m²), anchorTenant, footTrafficPerDay, parkingRatio (spaces per 100 m² GLA), vacancyRate (%), averageRentPerSqm (GHS/m²/annum).
  • ColdStorageFacilityid, propertyId, temperatureZones (TemperatureZone[]), hasBlasFreezer, backupPowerKW, certifications.
  • TemperatureZonename, 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).
  • ConstructionEquipmentid, 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 (A0A4), 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.
  • BIMModelid, 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.
  • ChangeOrderid, 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.
  • QCInspectionid, 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.
  • SafetyIncidentid, 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.

  • MaintenanceRequestid, 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.
  • Evictionid, 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.

  • MortgageApplicationid, 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.
  • CapitalCallid, 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.
  • CrowdfundingCampaignid, 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.
  • DiasporaInvestmentid, 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.

  • FactoryOrderid, 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.
  • FurnitureProductid, 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.
  • TileProductid, 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 | R9R13), 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.
  • RawMaterialSourceid, material, supplierName, supplierContact, origin, unit, unitCostGhs, priceVolatilityNote (optional), leadTimeDays, minimumOrderQty, qualityGrade, certification (optional), monthlyCapacityUnits, reliabilityScore (0–100, historical delivery performance), isPreferredSupplier, isActive, lastPriceReviewDate.
  • ProductionBatchid, 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).
  • QualityTestResultid, 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):

  • GlcApiCredentialsapiKey, bearerToken (optional OAuth 2.0 bearer, supersedes apiKey), clientId.
  • GlcTitleTypefreehold | leasehold | stool_land | family_land | vested_land | concession.
  • GlcStoolLandRegionGREATER_ACCRA | ASHANTI | EASTERN | CENTRAL | WESTERN | VOLTA | BRONG_AHAFO | NORTHERN | UPPER_EAST | UPPER_WEST (Office of the Administrator of Stool Lands regions).
  • GlcSearchStatuspending | in_review | completed | expired (result older than 3 months) | disputed (court caveat/injunction) | not_found.
  • GlcTitleSearchRequesttitleNumber, region, purpose (purchase_due_diligence | mortgage_security | litigation_support | internal_audit), requestingOrganisation, parcelId (optional).
  • GlcEncumbrancetype (mortgage | caveat | caution | restriction | court_order | lease_notation), holder, principalAmountGhs (optional), lodgedDate, expiryDate (optional), instrumentNumber, notes (optional).
  • GlcParcelBoundaryparcelId, officialAreaSqm, boundary (GeoJSON Polygon, WGS84/EPSG:4326), centroidGng (optional Ghana National Grid EPSG:25000 { easting, northing }), lastSurveyDate, surveyPlanRef.
  • GlcOwnershipRecordownerName, ownerId (masked Ghana Card/TIN), registrationDate, disposedDate (string | null), acquisitionMethod (purchase | gift | inheritance | court_order | government_grant), instrumentNumber.
  • GlcTitleSearchResultsearchReference, titleNumber, status, titleType, firstRegistrationDate, lastUpdatedDate, resultDate, expiryDate (3-month validity), currentOwners, ownershipHistory, encumbrances, parcel (optional), hasPendingApplications, isGeoreferenced, certificatePdfUrl (optional), digitalSignature (optional JWS compact serialisation).
  • GlcDeedRegistrationRequesttitleNumber, instrumentType (transfer | mortgage | lease | discharge | caveat | power_of_attorney), grantor/grantee ({ name, id }), considerationGhs (optional), executionDate, documentBase64, documentHash (SHA-256), witnesses, solicitor (optional).
  • GlcDeedRegistrationResponseapplicationNumber, status (submitted | under_review | queued_for_execution | registered | rejected), estimatedCompletionDate (optional), stampDutyGhs/ processingFeeGhs (optional), rejectionReason (optional), acknowledgementPdfUrl (optional).
  • GlcWebhookEventeventType (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_plotsid (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_buildingsid (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_floorsid, 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_plansid, 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_unitsid, 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_areasid, building_id FK, floor_id FK, name, area_type, area_sqm, access_level, maintained_by, spatial_path, boundary_json.
  • cybele_land_titlesid, 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_projectsid, 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_phasesid, 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_milestonesid, phase_id FK, name, planned_date, actual_date, is_critical_path, linked_payment_ghs.
  • cybele_quality_inspectionsid, 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_tenantsid, 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_leasesid, 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_schedulesid, 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_paymentsid, 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_ticketsid, 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_listingsid, 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_developmentsid (uuid PK, defaultRandom), name, type, status, lat/lon (numeric(9,6)), region, scale, year_completed, source, created_at/updated_at (timestamptz).
  • cybele_mortgage_applicationsid, 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_plotsid, 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_designsid, 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_ordersid, 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_recordsid, 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_fundsid, 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_itemsid, 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_snapshotsid, 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_investorsid, 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_propertiesid, 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_inventoryid, 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_bookingsid, 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_projectsid, 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_segmentsid, 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_logsid, 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.
  • CybelePubSubChannelsPROPERTY_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 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: secureHeaderscorsrequestId → 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.

  • CybeleTokenPayloadsub (user/service account ID), role, ownerId/tenantId/agentId (role-specific scoping), exp, iat, iss.
  • CybeleRoleowner, tenant, agent, contractor, developer, finance, admin.
  • Permissionproperty: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.

  • WsMessageTypeprogress_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.
  • WsMessagetype, 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/projectsProjectStage lifecycle (feasibilityplanningdesignconstructioncommissioninghandovercomplete); endpoints for dashboard, schedule/Gantt, daily reports, RFIs, change orders, QC, safety, earned value, documents, notifications.
  • apps/cybele/portfolioProperty (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/marketplaceListing (status active | under_offer | sold | let | withdrawn), Agent; endpoints for listings, search, recommendations, agents, inquiries/leads, viewings, comparison, market analytics, favourites.
  • apps/cybele/factoryProductionStation pipeline (scheduledframingrebarconcrete_pourcuringMEP_roughinsulationboard_upMEP_finishfinishingQC_inspectiondispatch_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 / PropertyResponseSchematype (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 / ProjectResponseSchemaname (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 / LeaseResponseSchemaunitId, 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 / MortgageDecisionSchemaapplicantId, 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 / ListingResponseSchematitle (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 / PropertyValuationResponseSchemapropertyId, 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 typesPropertyCreatedData, 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.
  • CybeleEventPublisherconnect, 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.

ConstructionEventKindbudget_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.

  • BudgetCommittedEventbudgetAtCompletionGHS, startDate, plannedCompletionDate, contractRef.
  • ProgressAttestedEventpercentComplete, attestedBy, attestedAt.
  • PaymentSettledEventclaimId, amountGHS, paidAt, payerAddress, payeeAddress.
  • MilestoneAchievedEventphaseId (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 formatdigitalAddressGps must match ^[A-Z]{2}-\d{3}-\d{4}$ (Ghana Post GPS), enforced in core validators and route schemas.
  • Coordinate reference integrityGeoLocation/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 orderingendDate 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 boundstenureMonths 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 identitynav_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.