Status: IMPLEMENTED. This specification is grounded in the source under
libs/cybele/*,apps/cybele/*, andlibs/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 eachproject.json). - Cybele API port:
4027; database schema name:cybele(fromlibs/cybele/README.md). - Phase reference for the build-out: TODO Phase 58 (
TODOS/phase-58.md); the Gaia weather/climate integration is Phase 175. Thedbschema sections carry inline phase subtask anchors such as58.1.2.6,58.1.2.9,58.1.2.11,58.1.2.12; the API modules carry58.1.3.xanchors.
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):
PropertyStatus—Planning,UnderConstruction,Completed,ForSale,ForRent,Occupied,UnderRenovation,Demolished.ConstructionStatus—Bidding,Awarded,Mobilization,InProgress,Substantial(substantial completion),Defects(defects liability period),FinalCompletion.LeaseType—FixedTerm,MonthToMonth,RentToOwn,GroundLease,TripleNet,GrossLease.PropertyType—Residential,Commercial,Industrial,MixedUse,Land,Hospitality,Healthcare,Education.ValuationMethod—Comparative,Income,Cost,DCF.ProjectPhaseType—Preconstruction,Foundation,Structure,MEP(mechanical/electrical/plumbing),Finishing,Handover.GhanaRegion— all 16 administrative regions of Ghana plus the legacyBrong_Ahafo:Greater_Accra,Ashanti,Western,Central,Eastern,Northern,Upper_East,Upper_West,Volta,Brong_Ahafo,Oti,Bono,Bono_East,Ahafo,Western_North,North_East,Savannah(17 values).ZoningClass—Residential_Low,Residential_Medium,Residential_High,Commercial,Industrial_Light,Industrial_Heavy,Mixed_Use,Agricultural,Conservation,Institutional.LandTenure—Freehold,Leasehold,Stool_Land,Family_Land,Vested— the customary and statutory tenure categories of Ghanaian land law.
Zod schemas wrapping each enum via z.nativeEnum(...) are exported from
validators.ts (PropertyStatusSchema, ConstructionStatusSchema,
LeaseTypeSchema, PropertyTypeSchema, ValuationMethodSchema,
ProjectPhaseTypeSchema, GhanaRegionSchema, ZoningClassSchema,
LandTenureSchema).
2.3 Spatial and address types#
All property and listing locations are expressed in Ghana-specific address and
geospatial types. The address type enforces Ghana Post GPS digital-address
formatting; the geospatial types always carry an explicit spatial reference ID
(srid) so coordinates are never stored without their coordinate system.
GhanaAddress—region(GhanaRegion),district,locality,streetName,houseNumber,digitalAddressGps,landmark,postalCode.digitalAddressGpsholds 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}$inGhanaAddressSchema.GeoLocation—latitude,longitude,elevation(metres above sea level),srid(Spatial Reference ID; default WGS-84 =4326).GeoPolygon—coordinates(ordered ring of[longitude, latitude]pairs),srid,area(m²),perimeter(m).
GeoLocationSchema constrains latitude to [-90, 90], longitude to
[-180, 180], and defaults srid to 4326.
2.4 Valuation and zoning#
Valuation records capture both the computed value and the methodology and assumptions behind it, enabling auditable reproductions of any past valuation. Zoning classifications carry the full set of planning constraints that site analysis and design checks evaluate against.
PropertyValuation—id,propertyId,method(ValuationMethod),value(GHS),currency,date,valuer(registered valuer/firm),confidence(0–1 coefficient),assumptions(string list),marketComparables(optionalMarketComparable[]).MarketComparable—address,saleDate,salePrice,area,pricePerSqm,adjustmentFactor,adjustedPrice.ZoningClassification—class(ZoningClass),permittedUses,maxDensity(dwelling units/hectare),maxHeight(m),setbackFront,setbackSide,setbackRear(m),farRatio(Floor Area Ratio).
2.5 Land title#
A LandTitle is the formal document that establishes ownership of a plot under
Ghanaian land law. Its expiryDate is only populated for leasehold titles;
freehold and stool-land titles do not expire. The isDisputed flag triggers
additional due-diligence checks in site analysis.
LandTitle — id (TitleId), titleNumber, registrationDate,
ownerId, ownerName, plotId (PlotId), encumbrances (string list),
area (m² per survey plan), surveyPlanRef, issueAuthority, expiryDate
(Date | null — only applicable to leasehold titles), isDisputed.
2.6 Property and property subtypes#
Property is the central domain object — the unit of valuation, portfolio
management, and PropTech listing. A base Property record holds fields common
to all property types. Four discriminated subtypes extend it, each adding fields
relevant to its specific use class. The type literal on each subtype allows
TypeScript to narrow to the correct shape. Duck-typed type guards in guards.ts
test for subtype by field presence rather than instanceof, so they work across
serialization boundaries.
Property (base) — id, type (PropertyType), status
(PropertyStatus), address (GhanaAddress), location (GeoLocation),
grossArea (m²), netArea (net usable, m²), currentValue (GHS),
lastValuation (PropertyValuation), titleId, yearBuilt, description,
amenities, images, createdAt, updatedAt.
Four discriminated subtypes extend Property, each pinned by its type
literal:
ResidentialProperty(type: Residential) —bedrooms,bathrooms,floorArea,hasGarden,parkingSpaces,hasPool,furnishing(furnished|semi-furnished|unfurnished),targetMarket(low-income|middle-income|high-income|luxury).CommercialProperty(type: Commercial) —officeGrade(A|B|C),floorPlates,columnSpacing,ceilingHeight,loadingDocks,generatorCapacity(kVA),fibreConnected.IndustrialProperty(type: Industrial) —warehouseArea,officeArea,loadingDocks,clearHeight(m),craneCapacity(tonnes; 0 if no crane),yardSpace,powerCapacity(kVA),hasRailAccess,hasColdStorage.MixedUseProperty(type: MixedUse) —retailPct,officePct,residentialPct,hospitalityPct(each 0–100),totalUnits,retailGLA(gross leasable area for retail, m²),anchorTenant.
guards.ts provides duck-typed (field-presence, not instanceof) type guards
for each property subtype and for the specialized facility types.
2.7 Plot, building, unit#
These three types form the physical hierarchy of a developed property: a Plot
is a parcel of land, a Building sits on a plot, and 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.
Plot—id(PlotId),titleId,area(m²),boundary(GeoPolygon),zoning(ZoningClassification),topography(flat|gentle_slope|steep_slope|undulating),soilType,encumbrances,isServiced(roads + power + water + sewer present),tenure(LandTenure),price(GHS),pricePerSqm(GHS).Building—id(BuildingId),propertyId,name,floors,totalUnits,grossArea,netLeasableArea,structuralSystem(rc_frame|steel_frame|load_bearing|timber),yearBuilt,condition(excellent|good|fair|poor),hasLift,hasBackupPower,hasBorehole.Unit—id(UnitId),buildingId,unitNumber,floor,area,type(studio|1br|2br|3br|4br|penthouse|office|retail|warehouse),status(available|occupied|maintenance|reserved),currentRent(contracted GHS/month),marketRent(estimated GHS/month),tenantId(TenantId | null).
2.8 Construction project, phase, milestone#
A ConstructionProject is broken into ordered ProjectPhase records, each
containing Milestone records. This three-level hierarchy (project → phase →
milestone) mirrors the standard PMI/FIDIC project breakdown. Milestones that
carry a linkedPayment value are payment milestones — when milestone_achieved
is recorded on the construction ledger, the linked amount is unlocked for
disbursement.
Milestone—id,name,plannedDate,actualDate(Date | null),isPredecessorComplete,completionCriteria,isCriticalPath,linkedPayment(GHS unlocked by this milestone, ornullfor a non-payment milestone).ProjectPhase—id,name(ProjectPhaseType),status(ConstructionStatus),plannedStart,plannedEnd,actualStart,actualEnd(nullable),percentComplete,cost(GHS),milestones(Milestone[]).ConstructionProject—id(ProjectId),name,type(residential|commercial|industrial|infrastructure|mixed_use),status(ConstructionStatus),clientId,propertyId,location(GhanaAddress),contractValue(original, GHS),revisedContractValue(including variations, GHS),startDate,plannedCompletionDate,forecastCompletionDate,percentComplete(0–100),projectManager,mainContractorId(ContractorId),consultants(architect/engineer/QS names),phases(ProjectPhase[]).
2.9 Contractor and subcontractor#
Contractor records represent main contractors; Subcontractor extends
Contractor with trade-specific fields. The registrationNumber is the Ghana
Revenue Authority or business registration number — a compliance requirement for
public procurement in Ghana. The bondingCapacity sets the maximum contract
value a contractor can be awarded.
Contractor—id(ContractorId),companyName,registrationNumber(Ghana Revenue Authority or business registration number),licenseType,licenseExpiry,specializations,rating(1–5, 5 = excellent),bondingCapacity(max GHS),completedProjects,ongoingProjects,contactEmail,contactPhone.SubcontractorextendsContractor— addstradeSpecialisation,crewSize,parentContractorId(ContractorId, optional),dailyCrewRateGhs,ownedEquipment(string list).
2.10 Tenant, lease, rent schedule#
The tenant and lease types model the full rental relationship. A Tenant
represents a person or company; a Lease binds a tenant to a specific unit for
a period; and RentSchedule is the period-by-period ledger generated from the
lease terms. The escalationType field determines how rent increases at each
review: a fixed percentage, CPI-linked (with a 4.5% CPI floor applied in the
API), or open-market review.
Tenant—id(TenantId),name,type(individual|corporate),contactEmail,contactPhone,idNumber(national ID, passport, or company registration number),creditScore(300–850),employerName,monthlyIncome(verified GHS),rentalHistory(good|fair|poor),createdAt.Lease—id(LeaseId),unitId,tenantId,type(LeaseType),startDate,endDate,rentAmount(GHS/month),rentCurrency(GHS|USD),escalationType(fixed_pct|cpi_linked|market_review),escalationRate(annual %),depositAmount,depositHeld,terms(string list),isActive,renewalOption,breakOption.RentSchedule—leaseId,periodStart,periodEnd,dueDate,amount(billed GHS),paid(received GHS),balance(amount − paid),status(current|overdue|paid|partial),daysOverdue,lateFee.
2.11 Finance primitives#
The finance primitives model the instruments used to finance property
acquisition and development. MortgageProduct defines the terms of a loan
product; AmortizationEntry is one row in a repayment schedule; REITFund is a
real-estate investment trust or fund; and DevelopmentProForma is the
project-level feasibility model showing cost, revenue, IRR, and equity multiple.
MortgageProduct—id,name,rateType(fixed|variable|hybrid),nominalRate,effectiveRate(annual %, fees/compounding included),maxLTV(max loan-to-value %),minTenureMonths,maxTenureMonths,minLoanAmount,maxLoanAmount,eligibilityCriteria.AmortizationEntry—period,paymentDate,openingBalance,payment,principal,interest,closingBalance.REITFund—id(FundId),name,ticker,nav(GHS),navPerUnit,unitsOutstanding,distributionYield(%),totalAssets,totalLiabilities,sectorAllocation(Record<string, number>, values must sum to 100),inceptionDate.DevelopmentProForma—projectId,landCost,hardCosts,softCosts,contingency,financingCosts,totalCost,grossRevenue,netRevenue,profit,profitMargin(% of total cost),irr(%),equityMultiple(net revenue / equity invested),paybackPeriod.
2.12 Manufacturing and materials primitives#
These types represent the physical products manufactured or sourced by Cybele's
factory and materials operations. PrefabModule is the base unit of modular
housing. The concrete, paint, steel, and tile product types carry Ghana
Standards Authority certification fields (gsCertified, ghanaStandard) that
are required for construction compliance in Ghana.
PrefabModule—id,designCode,type(studio|1br|2br|3br|classroom|clinic|office),widthM,lengthM,heightM,weightKg,structuralCapacity(kN/m² floor load),connections(array ofend|side|top|bottom),productionStatus(design|fabrication|qc|storage|transit|installed),unitCost.ConcreteBlock—dimensions(150x225x450mm|200x225x450mm|100x225x450mm),strengthNmm2(compressive, N/mm²),density(kg/m³),hasHollow,gsCertified(Ghana Standards Authority),batchNumber,testDate,testResult(pass|fail).PaintProduct—id,formulationCode,name,color,hexCode,finish(matte|eggshell|satin|semi_gloss|gloss),vocGPerL(volatile organic compounds, g/L),coverageM2PerL,dryingTimeHours,shelfLifeMonths.SteelProduct—id,profile(I_beam|H_beam|channel|angle|flat_bar|round_bar|hollow_section),gradeS(EN 10025:S275|S355|S420),lengthM,weightKgPerM,cross_section_area(cm²),millCertificate.BuildingMaterial—id(MaterialId),name,specificationCode,ghanaStandard(optional, Ghana Standards Authority code),unit,unitCostGhs,supplierName,supplierContact,leadTimeDays,minimumOrderQty,certificationBody(optional),certificationNumber(optional),isPrequalified,embodiedCarbonKgCo2(optional, kg CO₂e per unit),isGreenCertified.
2.13 Specialized facility types#
Beyond the four property subtypes, Cybele tracks additional managed-asset types
for specialized built-environment contexts. Each has a propertyId back-
reference where applicable, linking it to the base Property record for
portfolio management and valuation. Type guards for these are defined in
guards.ts.
types.ts defines additional managed-asset records, each with propertyId
back-references where applicable:
IndustrialPark—id,name,location,totalArea(hectares),developedArea,plots,vacantPlots,occupancyRate(%),isFreeZone(GIPC Free Zone status),anchorTenants,utilities(array ofpower|water|sewer|fibre|gas),masterPlanUrl.DataCenter—id,name,tierLevel(Uptime Institute Tier 1–4),powerCapacityMW,pueTarget(Power Usage Effectiveness),rackCount,totalFloorArea,colocationAvailable(m²),connectivity,certifications.HospitalityProperty—id,propertyId,name,starRating(1–5),roomCount,serviceType(hotel|serviced_apartment|resort|guesthouse),amenities,revpar(Revenue Per Available Room, GHS),adr(Average Daily Rate, GHS),occupancyRate(%).StudentHousing—id,propertyId,university,bedCount,distanceToMainGate(km),amenities,rentPerBedPerAnnum(GHS),occupancyRate(%),mealsIncluded.RetailProperty—id,propertyId,gla(gross leasable area, m²),anchorTenant,footTrafficPerDay,parkingRatio(spaces per 100 m² GLA),vacancyRate(%),averageRentPerSqm(GHS/m²/annum).ColdStorageFacility—id,propertyId,temperatureZones(TemperatureZone[]),hasBlasFreezer,backupPowerKW,certifications.TemperatureZone—name,minTemp,maxTemp,capacityPallets.
2.14 Construction supplementary types#
These types provide the detail behind a ConstructionProject. A
ConstructionTask is the atomic WBS unit tracked for earned-value purposes.
Blueprint and BIMModel record the drawing and model artefacts for a project.
ChangeOrder captures approved scope/cost/time changes with an approval chain.
QCInspection records the result of an on-site quality check. SafetyIncident
captures OSHA-classified site incidents with follow-up action tracking.
WeatherCondition captures daily site weather and its productivity impact —
this is the record Gaia data enriches at Phase 175.
ConstructionTask— the atomic WBS unit. Fields:id,projectId,projectPhaseId,wbsCode(PMI convention, e.g.1.2.3.4),description,durationDays(working days),predecessorIds(finish-to-start by default),plannedStart,plannedFinish,actualStart(optional),actualFinish(optional),percentComplete(0–100),resources(array of{ type: labour | plant | material, description, quantity, unit, unitCostGhs }),plannedValueGhs(BCWS — budgeted cost of work scheduled),isCriticalPath,totalFloat(slack, working days),notes(optional).ConstructionEquipment—id,name,category(tower_crane|mobile_crane|excavator|bulldozer|dumper|concrete_mixer|concrete_pump|compactor|generator|scaffolding|formwork|forklift|piling_rig|other),makeModel,capacityDescription,dailyRateGhs,weeklyRateGhs,operatorIncluded,availability(available|on_hire|under_maintenance|decommissioned),currentProjectId(optional),availableFrom(optional),safetyCertExpiry,ownerName,ownerContact.Blueprint— architectural/structural/MEP drawing.id,projectId,drawingNumber,revision,title,discipline(architectural|structural|civil|mechanical|electrical|plumbing|fire_protection|landscape),scale,format(A0–A4),fileUrl,fileSizeBytes,fileFormat(pdf|dwg|dxf|svg),issuedForConstructionDate(optional),status(draft|in_review|approved|issued_for_construction|superseded|void),drawnBy,checkedBy(optional),approvedBy(optional),createdAt,updatedAt.BIMModel—id,projectId,modelId(unique within the Common Data Environment),lod(Level of Development 100/200/300/350/400/500),discipline(architectural|structural|mep|civil|site|federated),softwareVersion,fileFormat(ifc|rvt|nwd|nwf|nwc|skp|rfa),ifcSchema(optional:IFC2X3|IFC4|IFC4X3),fileUrl,fileSizeBytes,coordinateSystem,elementCount(optional),status(draft|in_review|approved|published|archived),authoredBy,createdAt,updatedAt.ChangeOrder—id,projectId,voNumber,submittedDate,description,justification,costImpactGhs(positive = addition, negative = omission),scheduleImpactDays(positive = extension),raisedBy(client|contractor|consultant|statutory_authority),approvalChain(ordered array of{ approverName, approverRole, status: pending|approved|rejected, decisionDate?, comments? }),status(draft|submitted|under_review|approved|rejected|implemented),approvedCostGhs(optional),approvedScheduleDays(optional),supportingDocs.QCInspection—id,projectId,location,category(concrete_pour|rebar_placement|formwork|waterproofing|blockwork|roofing|plastering|tiling|electrical|plumbing|final_completion),inspectionDate,inspector,inspectorOrganisation,checklist(array of{ itemCode, description, result: pass|fail|n_a|observation, remarks? }),nonConformanceReports(array of{ ncrNumber, description, severity: minor|major|critical, responsibleParty, closureDate?, closureStatus: open|in_progress|closed| accepted_risk }),overallResult(pass|conditional_pass|fail|pending_retest),contractorSignoff(optional),clientSignoff(optional),attachments.SafetyIncident—id,projectId,incidentDateTime,severity(near_miss|first_aid|medical_treatment|lost_time|permanent_disability|fatality),oshaClassification(recordable|non_recordable|first_aid_only|fatality),description,location,injuredPartyName(optional),injuredPartyRole(optional),injuredPartyContractor(optionalContractorId),bodyPartAffected(optional),injuryType(optional),lostWorkDays,rootCauses(array ofunsafe_act|unsafe_condition|management_system_failure|environmental|equipment_failure),rootCauseDescription,immediateActions,preventiveActions(array of{ action, responsiblePerson, dueDate, status: open|in_progress|closed }),requiresRegulatoryNotification,regulatoryNotificationSentDate(optional),reportedBy,investigatedBy(optional),status(reported|under_investigation|closed|regulatory_review).WeatherCondition— daily site weather affecting productivity.id,projectId,date,temperatureCelsius({ min, max, avg }),humidityPct,windSpeedKmh,windDirection,precipitationMm,visibilityKm,weatherType(clear|partly_cloudy|overcast|light_rain|heavy_rain|storm|harmattan|fog),productivityMultiplier(1.0 = no impact, 0.5 = 50% loss),workStoppage,stoppageHours,dataSource(ghana_met|on_site_station|manual_entry),recordedBy(optional).
2.15 Tenant and lease supplementary types#
These types extend the core tenant/lease model with operational detail.
MaintenanceRequest is the helpdesk record raised against a unit.
ServiceCharge is the annual budget-and-allocation reconciliation for
common-area costs shared across tenants. TenantScreening is the pre-tenancy
due-diligence record. Eviction tracks the legally required
notice-and-proceedings process under the Ghana Rent Act.
MaintenanceRequest—id,propertyId,unitId(optional),tenantId(optional — null = raised by property manager),category(plumbing|electrical|hvac|structural|appliances|pest_control|cleaning|security|landscaping|other),description,priority(emergency|high|medium|low),reportedAt,targetCompletionDate(SLA target),completedAt(optional),assignedVendorId/assignedVendorName/assignedTechnicianName(optional),status(open|assigned|in_progress|pending_parts|completed|cancelled|on_hold),estimatedCostGhs/actualCostGhs(optional),costResponsibility(landlord|tenant|shared),satisfactionRating(optional 1–5),photos,notes.ServiceCharge— annual service-charge budget and per-tenant allocation.id,propertyId,periodStart,periodEnd,totalGlaM2,budgetItems(array keyed bycategory∈ {security,cleaning,electricity_common_areas,water_and_sewage,waste_management,landscaping,lift_maintenance,hvac_maintenance,building_insurance,management_fee,reserve_fund,other} withbudgetedGhsand optionalactualGhs),totalBudgetGhs,totalActualGhs(optional),allocationBasis(floor_area|equal_share|unit_count),tenantAllocations(per-tenant{ tenantId, unitId, tenantGlaM2, allocationPct, chargeGhs, isInvoiced, invoiceDate?, paidDate? }),status(draft|approved|invoiced|reconciled).TenantScreening— pre-tenancy assessment.id,prospectiveTenantId,propertyId,unitId(optional),initiatedDate,completedDate(optional),creditCheck({ provider, score, rating: exceptional|very_good|good|fair|poor, adverseItems, checkedDate }),employmentVerification({ employerName, employerContact, monthlyGrossIncomeGhs, employmentType: permanent|contract|self_employed| retired|unemployed, yearsEmployed, verified, verificationMethod: payslip|bank_statement|employment_letter|tax_return }),rentalReferences(array of prior-landlord records),identityVerification({ idType: ghana_card|passport|drivers_license|voters_id, idNumber, verified, verifiedBy? }),rentToIncomeRatio,affordabilityCheck,recommendation(approve|approve_with_conditions|reject|pending),conditions(optional),notes(optional),screenedBy.Eviction—id,leaseId,tenantId,propertyId,unitId,grounds(non_payment_of_rent|breach_of_tenancy_terms|property_damage|illegal_activities|owner_occupation|redevelopment|expiry_no_renewal),noticeServedDate,requiredNoticeDays(Ghana Rent Act notice period),vacatePossessionDate,rentArrearsGhs,status(notice_served|negotiation_in_progress|court_proceedings|court_order_obtained|bailiff_instructed|property_vacated|withdrawn),courtProceedings(optional nested record),settlement(optional nested record),actualVacateDate(optional),legalCostsGhs(optional),handledBy,notes.
2.16 Finance and investment supplementary types#
These types cover the full investment lifecycle beyond the basic finance
primitives. MortgageApplication is the underwriting record for a loan
application, including Bank of Ghana DTI compliance. InvestorAllocation tracks
a single investor's stake in a REIT or fund. EquityWaterfall defines the JV
distribution waterfall tiers. CapitalCall is the formal drawdown notice to
investors. CrowdfundingCampaign tracks a SEC Ghana–registered property
crowdfunding offering. DiasporaInvestment models overseas-resident investment
with remittance channels, AML checks, and Bank of Ghana approval references.
MortgageApplication—id,applicantId,applicantName,propertyId(optional),productId,loanAmountGhs,tenureMonths,annualRatePct,grossMonthlyIncomeGhs,monthlyDebtObligationsGhs,debtToIncomeRatio(Bank of Ghana guideline: ≤ 43%),creditScore(optional),employmentType(public_sector|private_sector|self_employed|diaspora),loanToValueRatio,supportingDocuments(array of{ type: payslip|bank_statement|tax_return|employment_letter| business_financials, fileUrl, verifiedAt?, verifiedBy? }),status(submitted|document_review|credit_assessment|property_valuation|offer_issued|offer_accepted|legal_processing|approved|rejected|withdrawn),underwritingNotes(optional),approvedAmountGhs/approvedRatePct(optional),offerIssuedDate/offerExpiryDate/decisionDate(optional),rejectionReasons(optional),createdAt,updatedAt.InvestorAllocation— investor stake in a REIT/fund.id,investorId,fundId,unitsAllocated,capitalCommittedGhs,capitalCalledGhs,uncalledCapitalGhs,distributionsReceivedGhs,currentNavGhs,totalReturnGhs,irr(optional, decimal),equityMultiple(optional),firstInvestmentDate,investorClass(retail|sophisticated|institutional|diaspora),kycStatus(pending|cleared|expired|flagged),distributionBankAccount(optional{ bankName, accountNumber, sortCode, currency: GHS|USD|GBP|EUR }),isActive.EquityWaterfall— JV distribution waterfall.id,fundId,name,preferredReturnRate(per-annum decimal),isCumulative,tiers(ordered array of{ tierNumber, name, description, lpShare, gpShare, irrHurdle? }),hasCatchUp(GP catch-up provision),returnOfCapitalFirst,waterfallType(american|european),targetIrr,createdAt,updatedAt.CapitalCall—id,fundId,callNumber,issuedDate,dueDate,totalAmountCalledGhs,purpose,investmentReferences,investorCalls(per-investor{ investorId, allocationId, amountCalledGhs, status: pending|paid|overdue|defaulted|excused, paidDate?, paymentReference? }),status(draft|issued|partially_funded|fully_funded|cancelled),noticeDocUrl(optional),createdBy,createdAt.CrowdfundingCampaign—id,propertyId,projectId(optional),title,description,targetAmountGhs,minimumInvestmentGhs,maximumInvestmentGhs(optional regulatory cap),expectedAnnualReturn(decimal),horizonMonths,startDate,closingDate,raisedAmountGhs,investorCount,status(draft|open|overfunded|funded|cancelled|completed),secApprovalReference(optional, SEC Ghana),offeringMemorandumUrl(optional),updates(array of{ date, title, body }),createdAt,updatedAt.DiasporaInvestment—id,investorId,residenceCountry(ISO 3166-1 alpha-2),sourceCurrency(GBP|USD|EUR|CAD|AUD|JPY|CHF|NOK|SEK),investmentType(direct_property|reit|crowdfunding|off_plan|land_banking),propertyId/fundId/campaignId(optional),amountSourceCurrency,exchangeRate,amountGhs,remittanceChannel(bank_transfer|mtn_momo|vodafone_cash|western_union|wise|remitly|ghipss),remittanceReference,fundsReceivedDate,fxHedgeReference(optional),amlChecks({ sourceOfFundsVerified, sourceOfFundsDocument?, pepScreeningClear, sanctionsScreeningClear, checkedDate, checkedBy }),graDeclarationRef(optional, Ghana Revenue Authority),bogApprovalRef(optional, Bank of Ghana — required for remittancesUSD 50,000),
status(pending_compliance|active|matured|exited|cancelled),createdAt,updatedAt.
2.17 Manufacturing and materials supplementary types#
These types model the full factory production workflow. A FactoryOrder is a
multi-line production order covering any mix of product types.
FurnitureProduct and TileProduct extend the materials catalog with
product-specific dimensions and certifications. RawMaterialSource tracks the
supplier side of procurement. ProductionBatch records a single manufacturing
run with yield calculation. QualityTestResult records an individual lab test
result referenced to an international standard.
FactoryOrder—id,customerId,projectId(optional),lineItems(array of{ productId, productType: prefab_module| concrete_block|steel|paint|tile|furniture|other, quantity, unit, unitCostGhs, lineValueGhs }),totalValueGhs,requestedDeliveryDate,committedDeliveryDate/actualDeliveryDate(optional),qualityGrade(standard|premium|bespoke),schedule(array of{ stage: design_confirmation|material_procurement|fabrication|qc_testing|finishing| dispatch, plannedStart, plannedEnd, actualEnd?, status: pending| in_progress|completed|delayed }),status(draft|confirmed|in_production|qc_hold|ready_for_dispatch|dispatched|delivered|cancelled),deliveryAddress(GhanaAddress),notes(optional),createdAt,updatedAt.FurnitureProduct—id,name,designCode,category(bedroom|living_room|dining|office|outdoor|kitchen|storage|bathroom),bom(bill of materials array),dimensions({ widthMm, depthMm, heightMm }),weightKg,primaryFinish,finishOptions,unitCostGhs,retailPriceGhs,assemblyInstructionsUrl(optional),gsaCertified,leadTimeDays,isActive.TileProduct—id,name,productCode,dimensions({ widthMm, lengthMm, thicknessMm }),material(ceramic|porcelain|granite|marble|slate|travertine|mosaic),finish(polished|matte|satin|textured|rustic|wood_look),colorFamily,slipRating(EN ISO 10545-17:A|B|C|R9–R13),waterAbsorptionPct(EN ISO 10545-3),breakingStrengthN(EN ISO 10545-4),suitableFor(array offloor|wall|outdoor|wet_area|heavy_traffic|commercial),coveragePerBoxM2,piecesPerBox,pricePerM2Ghs,supplierName,countryOfOrigin,gsaCertified,isActive.RawMaterialSource—id,material,supplierName,supplierContact,origin,unit,unitCostGhs,priceVolatilityNote(optional),leadTimeDays,minimumOrderQty,qualityGrade,certification(optional),monthlyCapacityUnits,reliabilityScore(0–100, historical delivery performance),isPreferredSupplier,isActive,lastPriceReviewDate.ProductionBatch—id,factoryOrderId,productId,productType,batchNumber,plannedQuantity,actualQuantity,rejectedQuantity,yieldPct((actual − rejected) / planned × 100),startTime,endTime,productionLine,supervisedBy,materialsConsumed(array of{ rawMaterialId, materialName, quantityUsed, unit }),qcTestIds,status(in_production|qc_testing|passed|failed|on_hold|scrapped),notes(optional).QualityTestResult—id,batchId/materialDeliveryId(optional),testType(compressive_strength|tensile_strength|water_absorption|slump_test|gradation|moisture_content|surface_hardness|impact_resistance|dimensional_check|coating_thickness|chemical_composition|fire_resistance|visual_inspection),specimenId,standardReference(e.g.BS EN 12390-3,ASTM C39,GS ISO 6892-1),measuredValue,unit,minimumRequired/maximumAllowed(optional),result(pass|fail|marginal),laboratoryName,laboratoryAccreditation(optional, e.g.GNBS,UKAS),testDate,testedBy,certificateUrl(optional),notes(optional).
2.18 Ghana Lands Commission (GLC) integration types#
The Ghana Lands Commission is the statutory authority responsible for land
administration and title registration in Ghana. Cybele integrates with the GLC
API to perform title searches, verify parcel boundaries, check encumbrances, and
register deeds. The types below model every request, response, and webhook event
in that integration. GlcParcelVerification is the aggregate KYC/AML result
that site analysis uses to flag parcels as clean, minor_issues,
major_issues, or do_not_proceed.
types.ts defines a dedicated block of interoperability types for the Ghana
Lands Commission API (title searches, parcel verification, deed registration,
encumbrance checks):
GlcApiCredentials—apiKey,bearerToken(optional OAuth 2.0 bearer, supersedesapiKey),clientId.GlcTitleType—freehold|leasehold|stool_land|family_land|vested_land|concession.GlcStoolLandRegion—GREATER_ACCRA|ASHANTI|EASTERN|CENTRAL|WESTERN|VOLTA|BRONG_AHAFO|NORTHERN|UPPER_EAST|UPPER_WEST(Office of the Administrator of Stool Lands regions).GlcSearchStatus—pending|in_review|completed|expired(result older than 3 months) |disputed(court caveat/injunction) |not_found.GlcTitleSearchRequest—titleNumber,region,purpose(purchase_due_diligence|mortgage_security|litigation_support|internal_audit),requestingOrganisation,parcelId(optional).GlcEncumbrance—type(mortgage|caveat|caution|restriction|court_order|lease_notation),holder,principalAmountGhs(optional),lodgedDate,expiryDate(optional),instrumentNumber,notes(optional).GlcParcelBoundary—parcelId,officialAreaSqm,boundary(GeoJSONPolygon, WGS84/EPSG:4326),centroidGng(optional Ghana National Grid EPSG:25000{ easting, northing }),lastSurveyDate,surveyPlanRef.GlcOwnershipRecord—ownerName,ownerId(masked Ghana Card/TIN),registrationDate,disposedDate(string | null),acquisitionMethod(purchase|gift|inheritance|court_order|government_grant),instrumentNumber.GlcTitleSearchResult—searchReference,titleNumber,status,titleType,firstRegistrationDate,lastUpdatedDate,resultDate,expiryDate(3-month validity),currentOwners,ownershipHistory,encumbrances,parcel(optional),hasPendingApplications,isGeoreferenced,certificatePdfUrl(optional),digitalSignature(optional JWS compact serialisation).GlcDeedRegistrationRequest—titleNumber,instrumentType(transfer|mortgage|lease|discharge|caveat|power_of_attorney),grantor/grantee({ name, id }),considerationGhs(optional),executionDate,documentBase64,documentHash(SHA-256),witnesses,solicitor(optional).GlcDeedRegistrationResponse—applicationNumber,status(submitted|under_review|queued_for_execution|registered|rejected),estimatedCompletionDate(optional),stampDutyGhs/processingFeeGhs(optional),rejectionReason(optional),acknowledgementPdfUrl(optional).GlcWebhookEvent—eventType(title_updated|encumbrance_lodged|encumbrance_discharged|ownership_transferred),titleNumber,eventTimestamp,transactionReference,signature(HMAC-SHA256),summary.GlcParcelVerification— aggregate KYC/AML result.parcelId,titleNumber,boundaryVerified,areaDeltaSqm(positive = seller overstated),hasUnresolvedDisputes,hasUnregisteredEncumbrances,riskRating(clean|minor_issues|major_issues|do_not_proceed),issues(array of{ code, severity: info|warning|critical, description }).
3. Persistence Schema#
Source: libs/cybele/db/src/schema.ts (Drizzle ORM, pg-core). The schema
targets PostgreSQL with PostGIS (a custom geometry(Polygon,4326) column type,
cybelePostgisPolygon, is defined). All tables are exported in a single
schema object alongside their relations. Decimal precision constants: area
numeric(10,2) / plot area numeric(12,2), money numeric(14,2).
The database schema name is cybele. The section is organized by functional
cluster: §3.1 lists all PostgreSQL enum types (which appear in column
definitions below); §3.2–3.10 cover each table group; §3.11 describes the
Drizzle relation definitions; §3.12–3.13 document connection pooling and Redis
configuration.
3.1 PostgreSQL enum types (pgEnum)#
Cybele defines database-native enum types so that column values are validated by
PostgreSQL itself, not only by application code. The enum names follow the
cybele_ prefix convention. The following enum types are defined in
schema.ts; values that are non-obvious are noted inline:
cybele_prop_status, cybele_prop_type, cybele_construction_status,
cybele_lease_type, cybele_escalation_type, cybele_ghana_region (17
values), cybele_zoning_class, cybele_land_tenure,
cybele_maintenance_priority (critical, high, medium, low),
cybele_inspection_category (11 values), cybele_mat_category (Cement,
Steel, Timber, Aggregates, Blocks, Roofing, Electrical, Plumbing,
Finishing, Prefab, Other), cybele_mat_qc_status (Pending, Passed,
Failed, OnHold, Quarantined), cybele_reit_fund_type (Equity,
Mortgage, Hybrid, Infrastructure), cybele_reit_fund_status
(Fundraising, Investing, Operating, Divesting, Closed, Wound_Up),
cybele_room_type (Standard, Deluxe, Suite, Executive, Presidential,
Studio, Serviced_Apartment, Dormitory), cybele_booking_status
(Inquiry, Provisional, Confirmed, Checked_In, Checked_Out, No_Show,
Cancelled), cybele_infra_type (14 values: Trunk_Road, Feeder_Road,
Urban_Road, Bridge, Culvert, Drainage, Water_Supply, Sewerage,
Power_Transmission, Telecoms, Airport, Port, Railway, Dam),
cybele_infra_project_status (Feasibility, Design, Procurement,
Construction, Commissioning, Operational, Decommissioned),
cybele_road_surface (Bitumen_Surface_Dressing, Asphalt_Concrete,
Concrete, Gravel, Laterite, Earth).
3.2 Property, plot, building, unit tables#
The property cluster stores the core built-environment hierarchy. The base
cybele_properties table is extended by type-specific tables via 1:1 FK
relationships, mirroring the TypeScript discriminated union in §2.6. Spatial
data lives in cybele_plots as both a GeoJSON JSONB column and a PostGIS
geometry(Polygon,4326) column (the latter is indexed for spatial queries).
Buildings subdivide into floors (cybele_building_floors), floor plans
(cybele_floor_plans), units, and common areas.
cybele_properties— base property record.id(uuid PK,defaultRandom),property_type,status,address_json(jsonb, not null),location_json(jsonb),gross_area_sqm,net_area_sqm,current_value_ghs,title_id,year_built,description,amenities(text[]),images(text[]),created_at,updated_at.cybele_residential_properties— 1:1 extension keyed byproperty_idFK →cybele_properties.bedrooms,bathrooms,floor_area_sqm,has_garden,parking_spaces,has_pool,furnishing,target_market.cybele_commercial_properties— 1:1 extension.office_grade,floor_plates_sqm,column_spacing_m,ceiling_height_m,loading_docks,generator_capacity_kva,fibre_connected.cybele_plots—id(uuid PK),title_id,area_sqm,boundary_geojson(jsonb),boundary_geometry(PostGISgeometry(Polygon,4326)),zoning,topography,soil_type,is_serviced,tenure,price_ghs,price_per_sqm.cybele_buildings—id(uuid PK),property_idFK,name,floors,total_units,gross_area_sqm,net_leasable_area_sqm,structural_system,year_built,condition,has_lift,has_backup_power,has_borehole.cybele_building_floors—id,building_idFK,level_number,level_name,usage_type,gross_area_sqm,net_area_sqm,elevation_m,spatial_path(hierarchical path string),sort_order.cybele_floor_plans—id,building_idFK,floor_idFK (→cybele_building_floors),plan_type,version,drawing_ref,storage_uri,file_format,scale,source_system,geometry_json(jsonb),is_current,issued_at,superseded_at.cybele_units—id,building_idFK,floor_idFK,unit_number,floor,area_sqm,unit_type,status,current_rent_ghs,market_rent_ghs,tenant_id,spatial_path,centroid_json,boundary_json.cybele_common_areas—id,building_idFK,floor_idFK,name,area_type,area_sqm,access_level,maintained_by,spatial_path,boundary_json.cybele_land_titles—id,title_number(unique, not null),registration_date,owner_id,owner_name,plot_idFK (→cybele_plots),encumbrances(text[]),area_sqm,survey_plan_ref,issue_authority,expiry_date,is_disputed.
3.3 Construction tables#
The construction cluster stores projects, their WBS breakdown, milestones, and
quality inspection records. The cybele_wbs_items table is self-referencing
(parent_item_id) to represent the WBS tree. Earned-value fields
(budgeted_cost_ghs, earned_value_ghs, actual_cost_ghs) are stored at the
WBS item level for detailed EVM reporting.
cybele_construction_projects—id,name,project_type,status(cybele_construction_status),client_id,property_idFK,location_json(jsonb, not null),contract_value_ghs(not null),revised_contract_value_ghs,start_date,planned_completion_date(not null),forecast_completion_date,percent_complete(real, default 0),project_manager(not null),main_contractor_id,consultants(text[]),created_at,updated_at.cybele_project_phases—id,project_idFK,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_idFK,phase_idFK,parent_item_id(self-reference for the WBS tree),wbs_code,name,description,item_type,status,responsible_org,planned_start/planned_end/actual_start/actual_end,duration_days,budgeted_cost_ghs,actual_cost_ghs,earned_value_ghs,percent_complete(real, default 0),predecessors_json(jsonb),metadata_json(jsonb),created_at,updated_at.cybele_milestones—id,phase_idFK,name,planned_date,actual_date,is_critical_path,linked_payment_ghs.cybele_quality_inspections—id,project_idFK,category(cybele_inspection_category),location,inspection_date,inspector,results_json(jsonb, not null),overall_status,ncrs(text[]).
3.4 Tenant, lease, rent, maintenance tables#
The tenant cluster stores the full rental relationship. cybele_leases links a
tenant to a unit; cybele_rent_schedules is the period-by-period billing ledger
derived from a lease; cybele_rent_payments records each received payment with
its mobile-money or bank channel. cybele_maintenance_tickets is the service
request log, with SLA target dates computed from priority at creation time
(critical = 1 day, high = 3 days, medium = 7 days).
cybele_tenants—id,tenant_type,name,contact_email,contact_phone(not null),id_type,id_number,address,employer_name,monthly_income_ghs,credit_score,rental_history,created_at,is_active(default true).cybele_leases—id,unit_idFK,tenant_idFK,lease_type(cybele_lease_type),start_date,end_date,rent_amount_ghs(not null),rent_currency(defaultGHS),escalation_type(cybele_escalation_type),escalation_rate(real),deposit_amount_ghs(not null),deposit_held(not null),renewal_notice_days(default 90),terms(text[]),is_active(default true),created_at,terminated_at.cybele_rent_schedules—id,lease_idFK,tenant_idFK,unit_idFK,period_number,period_start,period_end,due_date,amount_ghs,paid_ghs,balance_ghs,status,days_overdue(default 0),late_fee_ghs(default0),generated_at,last_payment_at.cybele_rent_payments—id,lease_idFK,schedule_idFK,tenant_id,amount_ghs(not null),channel,reference_number(not null),payment_date,status,note,confirmed_at.cybele_maintenance_tickets—id,property_id,unit_id,tenant_id,category,description(not null),priority(cybele_maintenance_priority),reported_at,target_completion_date(not null),assigned_vendor_id,status,actual_cost_ghs,estimated_cost_ghs,completed_at,notes.
3.5 Market and PropTech tables#
The market cluster stores the public-facing PropTech data: active property
listings, comparable-sale transactions used for valuation, competing
developments tracked for market intelligence, and mortgage applications.
cybele_market_transactions records are the comparable sales that the finance
modules adjust and index.
cybele_property_listings—id,title,description,property_type,transaction_type,price_ghs(not null),currency(defaultGHS),bedrooms,bathrooms,area_sqm(not null),district,region,latitude,longitude,amenities(text[]),images(text[]),agent_id(not null),status,published_at,view_count(default 0),inquiry_count(default 0),quality_score(default 0).cybele_market_transactions— comparable sale records.id,transaction_date,property_type,location,district,region,area_sqm,transaction_price_ghs,price_per_sqm,bedrooms,is_verified(default false).cybele_competitor_developments—id(uuid PK,defaultRandom),name,type,status,lat/lon(numeric(9,6)),region,scale,year_completed,source,created_at/updated_at(timestamptz).cybele_mortgage_applications—id,applicant_id,property_id,product_id,loan_amount_ghs,tenure_months,annual_rate_pct(real),gross_monthly_income_ghs,monthly_debt_obligations_ghs,credit_score,employment_type,status,approved_amount_ghs,approved_at,created_at.
3.6 Industrial-park and prefab tables#
Industrial-park plots subdivide a park into individually leasable land parcels
(distinct from the building Unit concept). Prefab production is tracked in
three linked tables: a module design catalog, individual production orders, and
per-module factory order items that trace through each production station.
On-site assembly tasks are linked back to the factory order item for full
traceability from module design to installed position.
cybele_industrial_park_plots—id,zone_id,area_m2,status,frontage_m,depth_m,monthly_lease_rate_ghs_per_sqm,tenant_id,tenant_name,allocated_power_kw,allocated_water_m3_per_day.cybele_prefab_module_designs—id,design_code(unique),module_type,version,description,external_dimensions_json(jsonb, not null),internal_dimensions_json(jsonb),finished_weight_kg,structural_capacity_kn_m2,stackable_floors,production_days(not null),unit_cost_ghs(not null),bom_json(jsonb),drawing_refs(text[]),assembly_instructions_url,is_active,created_at,updated_at.cybele_prefab_orders—id,design_idFK,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_idFK,design_idFK,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_idFK,order_item_idFK,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(default1),created_at,updated_at.cybele_material_inventory— per-warehouse stock.id,material_idFK,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_idFK,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, defaultPending),certification_ref,notes,created_at.cybele_material_qc_records—id,batch_idFK,tested_by,test_date,test_type,test_standard(e.g.BS EN 12390,ASTM C39),result,unit,min_acceptable,max_acceptable,status(cybele_mat_qc_status),lab_reference,report_url,created_at.
3.8 REIT / fund tables (phase anchor 58.1.2.9)#
The REIT cluster stores real-estate investment trust structures. A
cybele_reit_funds record is the top-level fund entity, referencing its SEC
Ghana registration. cybele_reit_portfolio_items links fund records to
underlying property assets. cybele_reit_nav_snapshots records periodic NAV
computations (NAV = gross asset value minus total liabilities). Investor records
track KYC status and distributions received.
cybele_reit_funds—id,name,ticker(unique),fund_type(cybele_reit_fund_type),status(cybele_reit_fund_status, defaultFundraising),target_size_ghs,raised_ghs(default0),inception_date,maturity_date,distribution_frequency(defaultquarterly),target_distribution_yield_pct,management_fee_pct(default1.5),performance_fee_pct(default20),custodian,trustee,sec_registration_ref(SEC Ghana),prospectus_url,created_at,updated_at.cybele_reit_portfolio_items—id,fund_idFK,property_idFK,acquisition_date,acquisition_cost_ghs,current_valuation_ghs,ownership_pct(default100),annual_noi_ghs,last_valuation_date,valuation_method,encumbered_ghs(mortgage debt on the asset, default0).cybele_reit_nav_snapshots—id,fund_idFK,snapshot_date,gross_asset_value_ghs,total_liabilities_ghs,nav_ghs(GAV − liabilities),units_outstanding,nav_per_unit_ghs(NAV / units),dividend_paid_ghs(default0),management_fee_ghs,computed_by,notes,created_at.cybele_reit_investors—id,fund_idFK,investor_name,investor_type,ghana_card_or_tin,units_held,subscription_price_ghs,subscription_date,total_invested_ghs,total_distributions_ghs(default0),bank_account_ref,kyc_status(defaultPending),created_at.
3.9 Hospitality tables (phase anchor 58.1.2.11)#
The hospitality cluster extends the base property record with hotel-specific
operational data. A cybele_hospitality_properties record (1:1 with a
cybele_properties record) carries PMS system references and channel manager
details. Room inventory and dynamic rate calendars are stored separately so rate
overrides can be managed without touching the room record. Booking confirmation
numbers are unique across the table for guest reference.
cybele_hospitality_properties—id,property_idFK (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(default14:00),check_out_time(default12:00).cybele_room_inventory—id,hospitality_property_idFK,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_idFK,room_type,valid_from,valid_to,rate_name(rack,corporate,group,advance_purchase, …),rate_ghs_per_night,breakfast_included,min_night_stay(default 1),advance_purchase_days,is_active,created_at.cybele_bookings—id,confirmation_no(unique),hospitality_property_idFK,room_idFK,guest_name,guest_email,guest_phone,guest_nationality,status(cybele_booking_status, defaultInquiry),check_in_date,check_out_date,adults(default 1),children(default 0),rate_ghs_per_night,total_nights,total_amount_ghs,deposit_paid_ghs(default0),balance_due_ghs,channel_source,special_requests,booked_at,updated_at.
3.10 Civil infrastructure tables (phase anchor 58.1.2.12)#
Civil infrastructure projects differ from building projects in that they have
linear assets (roads measured in chainages), governmental clients (client_name
e.g. GHA, GWCL, ECG), environmental impact permit references, and pavement
condition indices. Road segments store AASHTO structural numbers and California
Bearing Ratio values — the engineering parameters that determine pavement design
life. Maintenance logs track post-maintenance PCI scores to measure intervention
effectiveness.
cybele_civil_infra_projects—id,name,project_code(unique),infra_type(cybele_infra_type),status(cybele_infra_project_status, defaultFeasibility),region(cybele_ghana_region),district,contractor_name,consultant_name,client_name(not null — e.g. GHA, GWCL, ECG),contract_value_ghs,funding_source(GoG, World Bank, AfDB, …),commencement_date,original_completion_date(not null),revised_completion_date,actual_completion_date,length_km,area_ha,design_life_years,environ_impact_ref(EPA Ghana permit reference),created_at,updated_at.cybele_road_segments—id,project_idFK,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(defaultNot_Started),completion_pct(default0),created_at,updated_at.cybele_infra_maintenance_logs—id,project_idFK,segment_idFK,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) —PROPERTY600,LISTING300,SEARCH120,PRICE_INDEX3600,MARKET_SNAPSHOT1800,AGENT_SESSION86400,TENANT_PROFILE900,CONSTRUCTION_PROJECT300.CybelePubSubChannels—PROPERTY_UPDATE,LISTING_PUBLISHED,RENT_RECEIVED,MAINTENANCE_RAISED,PROJECT_MILESTONE(all under thecybele: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: secureHeaders → cors → requestId →
structured JSON logging → OpenTelemetry tracing → Prometheus request metrics →
request timeout → per-IP sliding-window rate limiter → JWT auth → (dev only)
Hono request logger. Public paths bypassing JWT auth: <basePath>/health,
<basePath>/metrics, <basePath>/listings, <basePath>/market-intel/prices.
The gateway mounts four REST route groups (/properties, /construction,
/leases, /materials) plus /health and /metrics. A 404 fallback returns
code CYBELE_404; the global error handler returns code CYBELE_500 with the
request ID.
CYBELE_SERVICE_REGISTRY is the service dispatch table mapping a route prefix
to a downstream service descriptor (name, routePrefix, healthPath):
property (/properties), construction (/construction), lease
(/leases), tenant (/tenants), materials (/materials), listings
(/listings, service cybele-proptech), finance (/finance), market-intel
(/market-intel).
4.2 Property REST routes (routes/properties.ts, anchor 58.1.3.2)#
The property routes provide CRUD over the property hierarchy plus valuation.
Each route is guarded by a requirePermission(...) middleware that checks the
caller's role against the RBAC matrix (§4.6). The route layer uses an injectable
PropertyRepository; in production, DrizzlePropertyRepository issues real SQL
through @cybele/db.
All routes live under /api/v1/properties:
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | / |
property:read |
List properties, filter + paginate |
| POST | / |
property:write |
Create a property |
| GET | /:id |
property:read |
Get property by UUID |
| PUT | /:id |
property:write |
Update property |
| DELETE | /:id |
property:delete |
Soft-delete (sets deletedAt) |
| GET | /:id/units |
property:read |
List units in the property's building |
| GET | /:id/valuation |
property:read |
Latest completed valuation |
| POST | /:id/valuation |
property:write |
Request an automated comparative valuation |
CreatePropertySchema validates propertyType (one of eight PropertyType
literals), status (one of seven non-Demolished literals), address (Ghana
address with the XX-NNN-NNNN digital-address regex), location (latitude
constrained to 4–12°N, longitude to −4–2°E — Ghana's bounding box),
grossAreaSqm (positive), netAreaSqm, currentValueGHS, titleId,
yearBuilt (1900 … current year + 5), description (10–5000 chars),
amenities, images (URLs). The list query supports type, status,
region, district, minArea/maxArea, minValue/maxValue, page (≥1),
limit (1–100, default 20), sort (createdAt | currentValueGHS |
grossAreaSqm), order (asc | desc). The route layer uses an injectable
PropertyRepository; setPropertyRepository(...) swaps the default
InMemoryPropertyRepository for a DrizzlePropertyRepository so handlers issue
real SQL through @cybele/db. The valuation estimator applies a
purposeOfValuation-dependent factor (0.97 for mortgage) and a
valuationType factor (0.82 for forced_sale), falling back to a region-based
price/m² (Greater Accra 5900, Ashanti 4500, else 3800 GHS/m²).
4.3 Construction REST routes (routes/construction.ts, anchor 58.1.3.3)#
The construction routes drive a project from creation through completion. Three
mutations write signed events to the hash-chained construction ledger (§7):
POST / records budget_committed; PUT /:id records progress_attested;
POST /:id/progress-claim records payment_settled. The earned-value endpoint
derives its metrics from that ledger rather than from ephemeral state.
All routes live under /api/v1/construction:
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | / |
construction:read |
List projects, filter + paginate |
| POST | / |
construction:write |
Create project (records budget_committed on the ledger) |
| GET | /:id |
construction:read |
Get project with current earned-value metrics |
| PUT | /:id |
construction:write |
Update progress (records progress_attested) |
| GET | /:id/phases |
construction:read |
List WBS phases |
| POST | /:id/phases |
construction:write |
Add a phase with milestones |
| PUT | /:id/phases/:phaseId |
construction:write |
Update phase progress; at 100% records milestone_achieved for each milestone |
| POST | /:id/milestones |
construction:write |
Record a milestone |
| GET | /:id/inspections |
construction:read |
List QC inspections, filter by category |
| POST | /:id/inspections |
construction:write |
Submit a QC inspection |
| GET | /:id/earned-value |
construction:read |
EVM metrics, optional ?asOf=<iso> |
| POST | /:id/progress-claim |
construction:write |
Submit a payment claim (records payment_settled) |
CreateProjectSchema validates name (3–200 chars), projectType
(residential | commercial | industrial | infrastructure | mixed_use),
propertyId (optional UUID), contractValueGHS (positive),
startDate/plannedCompletionDate (datetimes; completion must be after start),
projectManager, clientId, mainContractorId (optional), consultants
(optional), location ({ region, district, latitude?, longitude? }).
QualityInspectionSchema constrains category to the 11 inspection categories
and overallStatus to pass | conditional_pass | fail | pending_retest.
ProgressClaimSchema carries claimNumber, periodFrom/periodTo,
workDoneGHS, materialsOnSiteGHS, retentionPct (0–10, default 5),
variationOrders, submittedBy; the handler computes gross claim, retention
deduction, approved-variation total, and net-certified amount.
4.4 Lease and tenant REST routes (routes/leases.ts, anchor 58.1.3.4)#
The lease routes handle the full tenancy lifecycle — creation, renewal,
termination, payment recording, and arrears calculation. Maintenance tickets are
nested under leases because they are raised in the context of a tenancy. Tenant
routes are also grouped here. All routes live under /api/v1/leases.
Lease routes: GET / (list), POST / (create — lease:write), GET /:id
(with arrears), PUT /:id, POST /:id/renew, POST /:id/terminate,
GET /:id/payments, POST /:id/payments, GET /:id/arrears,
POST /:id/escalate.
Tenant routes: GET /tenants, POST /tenants, GET /tenants/:id (with
payment score), PUT /tenants/:id, DELETE /tenants/:id (deactivate),
GET /tenants/:id/leases, GET /tenants/:id/score.
Maintenance routes (nested): POST /:id/maintenance (maintenance:write),
GET /:id/maintenance (maintenance:read).
CreateTenantSchema validates tenantType (individual | company |
government | ngo), contactPhone (10–15 chars), idType (ghana_card |
passport | drivers_license | tin | company_reg), creditScore
(300–850). LeaseShapeSchema validates unitId/tenantId (UUIDs), leaseType
(six LeaseType literals), escalationType (fixed_pct | cpi_linked |
market_review), escalationRate (0–50%), renewalNoticeDays (30–365, default
90), rentCurrency (GHS | USD | EUR | GBP); CreateLeaseSchema refines
that endDate is after startDate. RecordPaymentSchema constrains channel
to mtn_momo | vodafone_cash | airteltigo | bank_transfer | cheque |
cash. TerminateLeaseSchema.reason is one of mutual_agreement |
breach_by_tenant | breach_by_landlord | property_sold |
property_redevelopment | non_payment | tenant_relocation.
MaintenanceTicketSchema.priority is critical | high | medium | low;
the handler sets targetCompletionDate to 1/3/7 days out depending on priority.
Arrears are computed from months elapsed since the last payment versus payments
made; escalation applies a CPI floor of 4.5% for cpi_linked leases and a +5%
premium for market_review leases.
4.5 Materials REST routes (routes/materials.ts, anchor 58.1.3.5)#
The materials routes manage the catalog, warehouse inventory, and production QC
lifecycle. Material SKUs and batch numbers are unique across their tables; the
route returns HTTP 409 on a duplicate. The low-stock alert endpoint and QC
summary dashboard aggregate across all materials, so they use a separate
/inventory/alerts and /batches/qc-summary path rather than nesting under a
material ID.
All routes live under /api/v1/materials:
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | / |
materials:read |
List material catalogue |
| POST | / |
materials:write |
Add a material (unique SKU; 409 on conflict) |
| GET | /:id |
materials:read |
Get material (by id or SKU) |
| GET | /:id/inventory |
materials:read |
Stock levels by location |
| PUT | /:id/inventory/:location |
materials:write |
Update stock at a location |
| GET | /:id/batches |
materials:read |
List production batches |
| POST | /:id/batches |
materials:write |
Create a production batch (unique batch number) |
| GET | /:id/batches/:batchId/qc |
materials:read |
QC records for a batch |
| POST | /:id/batches/:batchId/qc |
materials:write |
Submit a QC test result |
| GET | /inventory/alerts |
materials:read |
Low-stock alerts across all materials |
| GET | /batches/qc-summary |
materials:read |
QC pass-rate dashboard |
CreateMaterialSchema requires sku (uppercase alphanumeric, regex
^[A-Z0-9-]+$), category (one of the 11 cybele_mat_category values),
unitOfMeasure, unitCostGHS (positive), leadTimeDays (0–365, default 7).
QCTestSchema.testType is one of 15 test types; assessQCStatus returns
Pending when neither minAcceptable nor maxAcceptable is set, Failed when
the result is outside the band, otherwise Passed. A batch's qcStatus is
Failed if any test failed, Pending if there are no tests or any test is
pending, otherwise Passed. Batch cost is split into raw material / labour /
overhead with per-component percentages.
4.6 Authentication and RBAC (auth.ts, anchor 58.1.3.9)#
Cybele uses HS256 JWTs verified in-process. The token carries the caller's role,
and the ROLE_PERMISSIONS matrix maps each role to its allowed operations. The
requireOwnership guard adds a resource-level check on top of role-level
permission for owner-role callers. Below are the full token payload shape,
role list, permission list, and role-to-permission matrix.
Cybele uses HS256 JWTs verified in-process (verifyJwt) — header alg must be
HS256, the signature is checked with timingSafeEqual, the token must not be
expired, and the issuer must be cybele-auth.
CybeleTokenPayload—sub(user/service account ID),role,ownerId/tenantId/agentId(role-specific scoping),exp,iat,iss.CybeleRole—owner,tenant,agent,contractor,developer,finance,admin.Permission—property:read/write/delete,lease:read/write,maintenance:read/write,construction:read/write,materials:read/write,finance:read/write,listing:read/write,market-intel:read,admin:*.ROLE_PERMISSIONS— explicit grant matrix.owner: property read/write, lease read/write, maintenance read/write, listing read/write, finance read, market-intel read.tenant: property read, lease read, maintenance read/write, listing read.agent: property read, listing read/write, market-intel read, maintenance read.contractor: construction read/write, materials read, property read.developer: property read/write, construction read/write, materials read/write, finance read, market-intel read.finance: property read, finance read/write, market-intel read, construction read.admin:admin:*(grants everything).requirePermission(permission)throws 401 if unauthenticated, 403 if the role lacks the permission.requireOwnership(getResourceOwnerId)ensures anowner-role caller can only touch resources whereownerIdmatches;adminbypasses, other roles are not subject to the check.
4.7 Rate limiting (rate-limit.ts, anchor 58.1.3.10)#
Rate limits are tiered by role, with per-endpoint overrides for routes that are known to be expensive. The sliding-window counter uses Redis sorted sets and a 60-second window.
Sliding-window counter backed by Redis sorted sets. Per-role tiers in
RATE_LIMIT_TIERS (maxRequests per 60000 ms): anonymous 60, owner 200,
tenant 100, agent 500, contractor 200, developer 300, finance 200,
admin 2000. ENDPOINT_RATE_LIMITS adds stricter per-endpoint overrides:
GET /api/v1/properties 30/min (expensive PostGIS queries),
POST /api/v1/listings 10/min (anti-spam), POST /api/v1/finance 20/min
(CPU-bound), and a market-intel export override.
4.8 GraphQL API (graphql/schema.ts, anchor 58.1.3.6)#
The GraphQL API complements the REST surface for queries that span multiple aggregates or need flexible spatial filtering. It is particularly used by the marketplace app for listing search and by the portfolio app for cross-asset financial views.
@cybele/api exports a schema-first GraphQL SDL (typeDefs) and a typed
resolver map (resolvers, with ResolverContext and Resolvers types). The
SDL declares DateTime and JSON scalars; enums mirror the core enums
(PropertyType, PropertyStatus, ConstructionStatus, GhanaRegion,
MaterialCategory, TransactionType). Key object types: Property, Listing,
Transaction (comparable sale/rental), ConstructionProject, Lease,
Tenant, MarketPrice/PriceIndex, plus SpatialFilter input supporting
radius (centre + km), polygon (GeoJSON coordinates), district, and region
filtering.
Query fields: property, properties (with PropertyFilter + pagination),
listing, listings, listingsNearby (lat/lon/radiusKm, distance-ordered),
marketTransactions, priceIndex, constructionProject,
constructionProjects, tenant, tenants, lease, leases, reitFund,
reitFunds, material, materials. Mutation fields include
createProperty, updateProperty, createListing, updateListingStatus,
createLease, renewLease, terminateLease.
4.9 gRPC service definitions (grpc/definitions.ts, anchor 58.1.3.8)#
gRPC services expose Cybele's core operations as strongly-typed RPC interfaces,
suitable for service-to-service calls from other Oshun domains. Streaming RPCs
(SearchPropertiesNearby, StreamProjectUpdates, StreamPaymentEvents,
SendBulk) enable real-time data push without polling.
Five Protocol Buffers 3 service definitions are embedded as TypeScript string
constants and registered in CYBELE_GRPC_SERVICES with default ports:
| Service | Package | Port | RPCs |
|---|---|---|---|
PropertyService |
cybele.property.v1 |
50051 | GetProperty, ListProperties, CreateProperty, UpdateProperty, DeleteProperty, ComputeValuation, SearchPropertiesNearby (bidirectional stream) |
ConstructionService |
cybele.construction.v1 |
50052 | GetProject, UpdateProgress, ComputeEarnedValue, StreamProjectUpdates (server stream) |
LeaseService |
cybele.lease.v1 |
50053 | GetLease, RecordPayment, GetArrears, StreamPaymentEvents (server stream) |
FinanceService |
cybele.finance.v1 |
50054 | AssessMortgage, ComputeReitNav |
NotificationService |
cybele.notification.v1 |
50055 | Send, SendBulk (bidirectional stream) |
NotificationService defines NotificationChannel (SMS, WHATSAPP, EMAIL,
PUSH, IN_APP) and NotificationPriority (NORMAL, HIGH, CRITICAL).
4.10 WebSocket server (websocket/server.ts, anchor 58.1.3.7)#
The WebSocket server delivers real-time push updates to connected clients.
Clients subscribe to typed channels (e.g. project:proj-123) after
authenticating with a JWT on the upgrade request. Channel subscription is
authorized by the same role-based rules as the REST API.
CybeleWebSocketServer delivers real-time updates over ws. Clients
authenticate by passing a JWT in a ?token= query parameter (or
Authorization: Bearer header) on the upgrade. Channels follow the format
<type>:<id> with types project, lease, property, building, agent,
iot.
WsMessageType—progress_update,milestone_reached,inspection_submitted,variation_order,rent_received,rent_arrears_alert,maintenance_raised,maintenance_resolved,listing_enquiry,price_alert,iot_sensor_reading,iot_sensor_alarm,building_occupancy,ping,pong,subscribe,unsubscribe,error.WsMessage—type,channel,data,timestamp,correlationId(optional).canSubscribeToChannel(channel, userRole, userId)authorizes subscriptions per channel type:projectchannels are open tocontractor/developer/owner/admin;leasetotenant/owner/admin;propertytoowner/agent/contractor/developer/admin;buildingandiottoowner/developer/admin;agenttoagent/admin.adminmay 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 (currentv1, URL-path versioning).GET /logs,POST /logs— audit-log query and append (AuditLogEntrywithclientId,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. AWebhookSubscriptioncarriestargetUrl, aneventsarray, an HMAC signingsecret,isActive, and afailureCount;WebhookDeliveryrecordsevent,payload,status(pending|success|failed|retrying), andattempts.
4.12 Application service domains#
The four non-gateway apps each define a small in-memory domain layer (their own state machine types and in-memory stores) that sits above the library packages. This means each app can be deployed and tested independently without requiring a live database, while the library packages provide the domain logic. The key state machines per service are listed below:
apps/cybele/projects—ProjectStagelifecycle (feasibility→planning→design→construction→commissioning→handover→complete); endpoints for dashboard, schedule/Gantt, daily reports, RFIs, change orders, QC, safety, earned value, documents, notifications.apps/cybele/portfolio—Property(statusactive|under_development|vacant|disposed),Lease(statusactive|notice_given|expired|terminated),RentRecord; endpoints for portfolio summary, property detail, tenants, rent collection, maintenance, financials, valuation, lease calendar.apps/cybele/marketplace—Listing(statusactive|under_offer|sold|let|withdrawn),Agent; endpoints for listings, search, recommendations, agents, inquiries/leads, viewings, comparison, market analytics, favourites.apps/cybele/factory—ProductionStationpipeline (scheduled→framing→rebar→concrete_pour→curing→MEP_rough→insulation→board_up→MEP_finish→finishing→QC_inspection→dispatch_ready),ProductionOrder(statusdraft|confirmed|in_production|qc_hold|ready|dispatched|cancelled); endpoints for production orders, station scheduling, QC, material inventory, logistics, KPI dashboard, module configuration.
5. Contract Schemas#
Source: libs/contracts/cybele/src/api-schemas.ts. These Zod schemas are the
stable inter-domain contract surface and are independent of the runtime route
schemas in @cybele/api.
The distinction matters: the runtime route schemas in @cybele/api can evolve
with the service implementation; the contract schemas in @contracts/cybele are
the external promises Cybele makes to other domains. Other domains import only
from @contracts/cybele — never from @cybele/api or @cybele/core directly.
Each schema pair below defines what is acceptable for creating a record
(request) and what is guaranteed to be present in a response.
CreatePropertyRequestSchema/PropertyResponseSchema—type(six literals; note this contract omitsHealthcare/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 withid,currentValue,createdAt,updatedAt.CreateProjectRequestSchema/ProjectResponseSchema—name(3–200 chars),type(residential|commercial|industrial|infrastructure|mixed_use),propertyId(optional),contractValueGHS(positive),startDate/plannedCompletionDate,projectManager,clientId. The response addsid,status(draft|active|on_hold|completed|cancelled),progressPct(0–100),actualCostGHS,createdAt,updatedAt.CreateLeaseRequestSchema/LeaseResponseSchema—unitId,tenantId,type(sixLeaseTypeliterals),startDate/endDate(refined: end after start),rentAmountGHS(positive),depositAmountGHS,escalationType,escalationRate(0–50%). The response addsid,status(draft|active|expired|terminated|renewed),nextRentDue,totalArrearsGHS,createdAt,updatedAt.MortgageApplicationSchema/MortgageDecisionSchema—applicantId,propertyId,loanAmountGHS(positive),tenureMonths(12–360),productId,employmentType(salaried|self_employed|business_owner),grossMonthlyIncomeGHS,monthlyDebtObligationsGHS,creditScore(300–850). The decision carriesstatus(approved|conditional|declined|pending_docs),approvedAmountGHS,interestRatePct,monthlyInstallmentGHS,conditions,declinedReasons,debtToIncomeRatio,decidedAt.CreateListingRequestSchema/ListingResponseSchema—title(10–200 chars),description(50–5000 chars),propertyType(house|apartment|office|land|warehouse|retail|hotel),transactionType(sale|rent|lease),priceGHS,bedrooms/bathrooms(0–20),areaSqm,district,region,amenities,images(≥1 required). The response addsid,status(active|pending|sold|withdrawn|expired),viewCount,enquiryCount,publishedAt,createdAt,updatedAt.PropertyValuationRequestSchema/PropertyValuationResponseSchema—propertyId,valuationType(market_value|forced_sale|insurance_replacement|rental_value),purposeOfValuation(mortgage|sale|taxation|insurance|accounting),valuerId,inspectionDate. The response carriesestimatedValueGHS,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(literal1.0),id,source,type,subject,time(datetime),datacontenttype(literalapplication/json),data(unknown),correlationId(optional),businessUnit.CYBELE_EVENT_TYPES— the canonical event-type registry, all namedcybele.<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.
- Property:
CYBELE_KAFKA_TOPICS(contracts) —cybele.properties,cybele.construction,cybele.leases,cybele.finance,cybele.prefab,cybele.market.resolveKafkaTopic(eventType)routes a CloudEvents type to its topic by aggregate prefix.- Event payload types —
PropertyCreatedData,ProjectMilestoneData,SafetyIncidentData,LeaseExecutedData,RentPaymentData,RentArrearsData,MortgageApprovedData,CapitalCallData,DistributionData,PrefabModuleData,ListingCreatedData. For example,ProjectMilestoneDatacarriesprojectId,milestoneName,completionPct,plannedDate,actualDate,varianceDays;RentArrearsDatacarriesescalationLevel(warning|notice|legal). buildCybeleEvent(type, subject, data, businessUnit, correlationId?)constructs an envelope;validateCybeleEvent(event)safe-parses it againstCybeleEventSchema.
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-events12,cybele-construction-events6,cybele-lease-events6,cybele-finance-events3,cybele-iot-events24 (high-volume),cybele-notifications3,cybele-audit-events6. Partition key ispropertyId/tenantId/projectIdso every event for a resource lands on the same partition, preserving per-resource ordering.CybeleEventPublisher—connect,disconnect,publish,publishBatch; messages are GZIP-compressed and carryce-*CloudEvents headers.CybeleEventConsumer— registers per-event-type handlers viaon(), consumes, and routes failures to a dead-letter topic or handler (KafkaDeadLetterRecord,createDeadLetterRecord).createCybeleEvent(...)builds the CloudEvents envelope;createDefaultPublisher()reads brokers fromKAFKA_BROKERS.
The runtime publisher also exports Prometheus counters kafkaMessagesPublished
and kafkaMessagesConsumed.
7. Construction On-Chain Ledger#
Source: libs/cybele/api/src/routes/construction-ledger.ts. The Earned Value
Management endpoint (GET /construction/:id/earned-value) does not trust
ephemeral route state — it derives its inputs from an authoritative,
hash-chained, signed ledger.
The ledger exists because construction finance depends on tamper-evident
records: lenders and investors need to know that a budget commitment, a progress
attestation, or a payment settlement has not been retroactively altered. The
hash-chain design (each event commits to the previous event's hash) means any
modification to a past event changes every subsequent hash and is detected by
verifyChain. The default InMemoryConstructionLedger is suitable for testing;
production deployments swap in an EVM/JSON-RPC backend via
setConstructionLedger(...).
7.1 Ledger events#
Each ledger event corresponds to one EVM-relevant construction mutation. The four event kinds capture the four moments that matter to project finance: committing a budget, attesting progress, settling a payment, and achieving a milestone. The hash and signature fields make each event self-authenticating.
ConstructionEventKind — budget_committed, progress_attested,
payment_settled, milestone_achieved. Each event extends OnChainEventBase:
kind, projectId, recordedAt, transactionHash (0x + SHA-256 of
canonical event data), blockNumber (monotonically increasing), prevHash
(previous on-chain hash for this project, forming a per-project Merkle-style
chain — 0x000…0 at genesis), signature (ECDSA secp256k1 over the canonical
data), validatorAddress.
BudgetCommittedEvent—budgetAtCompletionGHS,startDate,plannedCompletionDate,contractRef.ProgressAttestedEvent—percentComplete,attestedBy,attestedAt.PaymentSettledEvent—claimId,amountGHS,paidAt,payerAddress,payeeAddress.MilestoneAchievedEvent—phaseId(optional),milestoneId,achievedAt,linkedPaymentGHS(optional).
ConfirmedConstructionEvent projects an event with a confirmations count
(currentBlock − blockNumber + 1).
7.2 Ledger contract and backend#
ConstructionLedger defines recordBudget, recordProgress, recordPayment,
recordMilestone, getProjectEvents, getCurrentBlock, verifyChain
(validates the per-project hash chain and every signature, returning the failing
index and reason on a break), and reset. The default
InMemoryConstructionLedger keeps the chain in process memory with a
deterministic signing secret and a genesis block (12_000_000).
setConstructionLedger(...) swaps in a real EVM/JSON-RPC backend. The route
layer wires ledger writes to the four EVM-relevant mutations (create-project →
budget_committed, update-progress → progress_attested, progress-claim →
payment_settled, phase-complete → milestone_achieved).
7.3 EVM derivation#
Earned Value Management requires four input scalars: Budget at Completion (BAC), Planned Value (PV), Earned Value (EV), and Actual Cost (AC). Cybele derives these from the authoritative ledger — not from the database state — so the EVM computation is reproducible and auditable at any point in time.
deriveEvmInputs(...) aggregates confirmed ledger events into the four EVM
scalars: budgetAtCompletion (most recent confirmed budget_committed; falls
back to the in-memory contract value, marked source: 'in_memory_fallback'),
percentComplete (latest confirmed progress_attested, else derived from
achieved-milestone count), actualCostToDate (sum of confirmed
payment_settled.amountGHS with paidAt ≤ asOf), plannedValueToDate (linear
S-curve of BAC × elapsed-fraction). computeEarnedValue(...) then produces
the standard EVM metrics: earned value, schedule variance, cost variance, SPI,
CPI, TCPI, EAC (estimate at completion), ETC, VAC (variance at completion), and
scheduleStatus/costStatus classifications.
8. Cross-Domain Integration#
Source: libs/cybele/integration/src/* (brigid-bridges, saraswati-bridges,
asase-bridges, freya-bridges, maat-bridges), re-exported from index.ts.
All exchange flows through @cybele/integration and @contracts/cybele;
consumers never import Cybele internals.
Each bridge is a dedicated adapter module responsible for translating between Cybele's internal types and the stable contract types used by the peer domain. This means a change to Cybele's internal schema only requires updating the bridge, not the consuming domain. Cross-domain endpoint URLs are environment-driven (see §9) so the same bridge code works in every deployment environment.
- Brigid — supplies industrial automation, energy, maintenance, and materials/manufacturing intelligence for Cybele factories and facilities.
- Saraswati — supplies advanced-technology manufacturing, energy devices, telecom, IoT, and security-systems intelligence.
- Asase — consumes Cybele infrastructure and site-planning data for agricultural facilities.
- Freya — consumes Cybele retail, manufacturing, and real-estate intelligence for luxury stores, workshops, and factories.
- Maat — consumes Cybele financials, risk, and capital-planning data.
Cross-domain endpoint URLs are environment-driven (see §9): BRIGID_API_URL,
SARASWATI_API_URL, ASASE_API_URL, FREYA_API_URL, MAAT_API_URL.
Phase 175 additionally adds Gaia storm, cyclone, precipitation, heat, wind,
urban-downscaling, and climate-scenario products as inputs to Cybele (pre-storm
asset-hardening alerts, construction-site weather risk, drainage and flood
planning, urban heat-island analysis, climate adaptation, schedule risk, and
portfolio resilience). The WeatherCondition core type (§2.14) is the on-site
weather record those products enrich.
9. Configuration and Environment#
Source: libs/cybele/README.md and the gateway/Kafka modules.
All Cybele services read configuration from environment variables at startup.
The table below lists every variable, its purpose, and the default value where
one is defined in code. The local development stack is started with
docker compose -f docker/docker-compose.cybele.yml up -d.
| Variable | Purpose |
|---|---|
CYBELE_DATABASE_URL |
PostgreSQL connection string (?schema=cybele) |
REDIS_URL |
Redis URL for caching and rate-limit state (default redis://localhost:6379) |
KAFKA_BROKERS |
Comma-separated Kafka broker list (default localhost:9092) |
JWT_SECRET |
HS256 secret for verifying access tokens |
CYBELE_DOCUMENT_BUCKET |
Object-storage bucket for documents |
CYBELE_DRAWINGS_BUCKET |
Object-storage bucket for drawings/BIM files |
CYBELE_SITE_PHOTOS_BUCKET |
Object-storage bucket for site photos |
GHANA_LANDS_COMMISSION_API_URL |
Ghana Lands Commission API base URL |
GHIPSS_API_URL |
Ghana Interbank Payment and Settlement Systems API |
MOBILE_MONEY_API_URL |
Mobile-money payment API |
BRIGID_API_URL / SARASWATI_API_URL / ASASE_API_URL / FREYA_API_URL / MAAT_API_URL |
Cross-domain service endpoints |
The local infrastructure stack is started with
docker compose -f docker/docker-compose.cybele.yml up -d.
10. Invariants and Hard Requirements#
These constraints are enforced in the code cited above and apply across the entire domain. They are not optional — violating any of them would produce incorrect financial results, invalid geospatial data, or compromised audit trails. Engineers adding new routes, schemas, or migrations must verify that these invariants are preserved.
- Ghana geographic bounds — property and listing
locationinputs are validated to Ghana's bounding box: latitude 4–12°N, longitude −4–2°E (CreatePropertySchema,CreatePropertyRequestSchema). - Digital-address format —
digitalAddressGpsmust match^[A-Z]{2}-\d{3}-\d{4}$(Ghana Post GPS), enforced incorevalidators and route schemas. - Coordinate reference integrity —
GeoLocation/GeoPolygoncarry an explicitsrid(default 4326); the PostGISboundary_geometrycolumn is declaredgeometry(Polygon,4326). Spatial data is never stored or computed without its CRS. - Lease date ordering —
endDatemust be strictly afterstartDate(CreateLeaseRequestSchemaandCreateLeaseSchemarefinements); constructionplannedCompletionDatemust be afterstartDate, and a phase'splannedEndafter itsplannedStart. - Escalation bounds — lease
escalationRateis constrained to 0–50% per annum. - Mortgage bounds —
tenureMonths12–360,creditScore300–850; underwriting tracksdebtToIncomeRatioagainst the Bank of Ghana ≤ 43% guideline. - Auditable construction changes — every EVM-relevant change appends a
signed, hash-chained event to the construction ledger;
verifyChaindetects any tampering by reporting the first failing event index and reason. - REIT NAV identity —
nav_ghsis gross asset value minus total liabilities;nav_per_unit_ghsis NAV divided by units outstanding;REITFund.sectorAllocationpercentages 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/batchidentifiers 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 (
deletedAtis 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.