Built-environment intelligence for Ghana's property lifecycle — land, design, construction, operations, finance, and PropTech — implemented as a TypeScript/Nx workspace under
libs/cybele/*,libs/contracts/cybele, andapps/cybele/*. (Scope tagscope:cybele; API port4027; database schemacybele; build-out tracked as TODO Phase 58.)
What Cybele Is, and Why It Exists#
Cybele is the Oshun bounded context for the built environment. It models the
full lifecycle of property as a single connected system: a parcel of land is
analysed for suitability, designed against a building code, delivered through a
construction project, operated as income-producing real estate, financed with
mortgages and REIT capital, and listed on a PropTech marketplace. The same
identity threads through all of it — a PlotId analysed in site-analysis is the
plot a Building is constructed on, whose Units carry the Leases that a
portfolio values and that a marketplace listing advertises.
The real-world problem is fragmentation. In Ghana, land registries, construction
delivery, tenant leases, and investment portfolios live in disconnected silos,
so integrated suitability scoring, cost tracking, and portfolio valuation are
impossible to do across the asset's life. Cybele's answer is a shared type
system (@cybele/core) plus geospatial-aware persistence (@cybele/db) plus
stable cross-domain contracts (@contracts/cybele), so that every score,
milestone, lease, and projection references one property and plot identity.
Cybele is a TypeScript domain, not a Rust engine — it sits in the
service/application tier of the stack, not the runtime kernel tier (contrast
Neith). It depends on workspace infrastructure (Hono, Drizzle, kafkajs, Zod,
node:crypto) and is consumed by sibling business domains — Brigid (factory
automation inside facilities Cybele tracks as assets), Asase (agricultural-site
infrastructure), Freya (real-estate envelopes for luxury retail/manufacturing),
and Maat (organization-wide capital and governance rollups) — through the
contract package and the integration bridges, never through Cybele internals.
Workspace Shape#
The domain is nineteen library packages, one contracts package, and five deployable application services:
libs/cybele/
core/ common/ db/ api/ ← foundation
site-analysis/ design/ construction/ ← deliver
property/ industrial-parks/ hospitality/ ← operate
infrastructure/
prefab/ materials/ ← manufacture
finance/ financials/ market-intel/ ← finance & analyse
proptech/ ← market
integration/ testing/ ← cross-cutting
libs/contracts/cybele/ ← stable inter-domain schemas + events
apps/cybele/ api/ portfolio/ projects/ marketplace/ factory/
The libs/cybele/api directory is the largest single package (34 tracked
files); site-analysis (26) and design (22) follow. The five apps are thin
Hono surfaces — each is a src/index.ts plus a single src/routes/<name>.ts
file — that compose library logic into one operational workflow apiece.
Architectural Layers#
The packages form five layers that build strictly upward.
1. Core type system — @cybele/core. The canonical vocabulary. Source
libs/cybele/core/src/types.ts (re-exported from index.ts alongside
validators, guards, serializers) defines twelve branded ID types
(PropertyId, PlotId, BuildingId, UnitId, TitleId, ProjectId,
TenantId, LeaseId, ContractorId, MaterialId, InvestorId, FundId —
each string & { readonly __brand } with a make… factory), the string-valued
state-machine enums (PropertyStatus, ConstructionStatus, LeaseType,
ProjectPhaseType, ZoningClass, LandTenure, the 17-value GhanaRegion),
Ghana-specific spatial/address types (GhanaAddress with the Ghana Post GPS
regex ^[A-Z]{2}-\d{3}-\d{4}$, GeoLocation/GeoPolygon carrying an explicit
srid), and every domain aggregate (Plot, Property + four discriminated
subtypes, ConstructionProject/ProjectPhase/Milestone, Lease, finance
primitives, materials, specialized facilities, and the Ghana Lands Commission
API interop types). validators.ts wraps each enum in a Zod z.nativeEnum(...)
schema; guards.ts provides duck-typed (field-presence, not instanceof) type
guards that survive serialization boundaries. Every other package imports from
here; nothing here imports from any other Cybele package.
2. Shared utilities — @cybele/common. Math, date, geo, ID helpers, plus a
substantial carbon-esg.ts whole-life carbon calculator (EN 15978 lifecycle
stages A1–A3 … D, Ghana-sourced material emission factors, GRESB/TCFD-aligned
reporting). Pure functions, no I/O.
3. Data — @cybele/db. The Drizzle/PostgreSQL schema. src/schema.ts
declares 44 pgTables and 19 pgEnums, plus a custom Postgis column type
cybelePostgisPolygon rendering geometry(Polygon,4326) for plot boundaries.
Nine numbered migrations (drizzle/0000…0008) cover the initial schema,
TimescaleDB IoT hypertables, partitioning, the PostGIS boundary, the spatial
building hierarchy, construction WBS, rent schedules, prefab/orders/assembly,
and competitor developments. connection.ts defines per-service-role PgBouncer
PoolProfiles (ecommerce, construction, property, analytics, iot,
hospitality, finance, admin) and a generatePgBouncerIni(...) config
generator; redis-config.ts defines the cybele: key builders, per-entity
TTLs, and cybele:events:* pub/sub channels. These are configuration and
helpers — they describe how infrastructure is wired, they do not themselves run
PgBouncer or Redis.
4. Capability libraries. Each real-estate or construction subdomain owns its algorithms on top of core + db. These are the bulk of the domain logic and are genuinely domain-specific, not CRUD:
site-analysis— multi-criteria decision analysis (MCDA) site scoring with sensitivity analysis and RICS highest-and-best-use (selection.ts); DEM topography via Horn's slope/aspect method (dem-provider.ts); demographics, infrastructure access, flood/seismic/air-quality risk; land-registry checks; ML/NLP-flavouredai-selection.ts.design— structural, MEP, sustainability, BIM (bim.ts), drawings, andgenerative.ts(parametric massing/unit-mix optimisation under setback/plot- ratio constraints) plus a BIM-to-digital-twin.tspipeline.construction— CPM forward/backward pass and PERT (scheduling.ts, PMI/PMBOK), cost/earned-value, procurement, quality, progress, and drone/ LiDAR safety inference (drone-inference.ts,ar-lidar.ts).property— leases, tenants, maintenance, valuation, analytics, smart-buildingiot.ts.prefab+materials— modular module catalogs, factory production, affordability, logistics; concrete/steel/paint/tile/furniture product physics with Ghana Standards Authority certification fields.finance+financials— Ghana mortgage products and amortisation (mortgage.ts, with real 2024–2026 GHB/Republic/diaspora rate data), REIT NAV, development pro formas, crowdfunding, cross-business-unit cost/synergy models.market-intel— price/rent indices, comparables, supply pipeline, economic indicators.industrial-parks,hospitality,infrastructure— the distinct property classes (utility-capacity-bound parks, hotel asset/RevPAR operations, linear civil assets with governmental clients).proptech— listings, CRM, digital experience, transactions, and a cryptographicblockchain-title.tsregistry (Ethereum-styleWallet, keccak256, EIP-191, secp256k1 ECDSA overnode:crypto).
5. Contracts, API gateway, and applications. @contracts/cybele is the
stable boundary (schemas + events, below). @cybele/api is the runtime gateway
(Hono + auth + Kafka + GraphQL + gRPC + WebSocket + the construction ledger).
The five apps/cybele/* services compose all of the above into deployable
surfaces.
The API Gateway#
createGateway(config) in libs/cybele/api/src/gateway.ts builds a single Hono
app rooted at /api/v1. The middleware pipeline (in order) is secure-headers →
CORS → request-id → structured JSON logging → OpenTelemetry tracing → Prometheus
metrics → request timeout → per-IP sliding-window rate limit (Redis-backed, 200
req/min default) → JWT auth. Four paths bypass auth: /health, /metrics,
/listings, and /market-intel/prices (public reads).
Honest scope note: the gateway mounts four REST route groups —
propertiesRouter, constructionRouter, leasesRouter, materialsRouter —
which are the four primary-aggregate REST families. The
CYBELE_SERVICE_REGISTRY constant enumerates eight logical service slots
(adding tenant, listings, finance, market-intel); those four extra slots
are a dispatch/health-aggregation table and are served through the GraphQL
endpoint and the application services rather than by a mounted REST router in
createGateway. The propertiesRouter writes through a PropertyRepository
interface with two implementations — DrizzlePropertyRepository (real Postgres)
and InMemoryPropertyRepository (default, swappable via
setPropertyRepository) — so the route logic is testable without a database.
The gateway also exposes GraphQL (graphql/schema.ts — typeDefs +
resolvers), five protobuf service definitions (grpc/definitions.ts:
property, construction, lease, finance, notification), and a WebSocket server
(websocket/server.ts) whose canSubscribeToChannel(channel, role) gates
project:<id> and agent channels by role. The deployable apps/cybele/api is a
thin wrapper: it serves /health and /version (0.2.0) and mounts a
/gateway router that adds gateway-management endpoints (routing table, client
tokens, per-client rate limits, audit log, OpenAPI doc, webhook subscriptions).
Domain Events#
@contracts/cybele/src/events.ts is the event canon. It defines the CloudEvents
1.0 envelope (CybeleEventSchema), a registry CYBELE_EVENT_TYPES of 31
event types across six aggregates (property 5, construction 7, lease 6,
finance 5, prefab 4, market 4), buildCybeleEvent(...) /
validateCybeleEvent(...) factory+validator, and resolveKafkaTopic(...) that
maps an event type to one of six logical topics (cybele.properties,
cybele.construction, cybele.leases, cybele.finance, cybele.prefab,
cybele.market) by prefix.
The runtime in libs/cybele/api/src/kafka.ts uses a second, physical topic
registry — cybele-property-events, cybele-construction-events,
cybele-lease-events, cybele-finance-events, cybele-iot-events,
cybele-notifications, cybele-audit-events — with recommended partition
counts (IoT highest at 24). CybeleEventPublisher/CybeleEventConsumer wrap a
real kafkajs client: GZIP compression, CloudEvents headers (ce-*), partition
key = partitionKey ?? subject (so all events for one property/tenant/project
stay ordered), retries, a dead-letter path (createDeadLetterRecord → DLQ
topic or handler), and consumer errors are swallowed after DLQ to avoid group
rebalance. A broker is required at runtime; createDefaultPublisher() reads
KAFKA_BROKERS. The two registries are an intentional split: contracts expose
stable logical names to consumers, the runtime owns physical topic/partition
layout.
The Construction Ledger#
libs/cybele/api/src/routes/construction-ledger.ts is the most
security-sensitive component. Schedule/budget mutations append a hash-chained,
signed event to a per-project chain: budget_committed (POST /construction),
progress_attested (PUT /construction/:id), payment_settled (POST
…/progress-claim), milestone_achieved (PUT …/phases/:phaseId at 100%). Each
event carries a sha-256 transactionHash, an incrementing blockNumber, a
prevHash linking it to the previous event, and an ECDSA secp256k1
signature (validator key deterministically derived from a signing secret via
createECDH('secp256k1') + JWK import). verifyChain(projectId) re-walks the
chain and returns the offending index + reason (prev_hash_mismatch /
tx_hash_mismatch / invalid_signature) on tamper. deriveEvmInputs(...)
aggregates confirmed events into the four Earned-Value scalars (BAC, %-complete,
AC, PV) the EVM endpoint needs, with a linear S-curve planned-value
approximation (documented as the Ghana FIDIC default absent a cost-loaded
schedule).
This is a real cryptographic ledger, but the default backend is
InMemoryConstructionLedger — the chain lives in process memory and is what the
route unit tests run against. The ConstructionLedger interface plus
setConstructionLedger(...) is a fail-loud seam: a production deployment
injects an EVM/JSON-RPC client wrapping the real construction smart-contract
suite. The crypto is identical to proptech/blockchain-title.ts; the two
intentionally duplicate ~20 lines of signing helpers rather than create a
@cybele/api → @cybele/proptech dependency cycle.
Provider Seams (Implemented vs Deployable)#
Several capability libraries that touch heavy external models follow one
pattern: a Fixture… backend (deterministic, pure-TS, in-memory) plus a
production backend that dynamic-imports a native binding.
construction/drone-inference.ts has FixtureDroneInferenceBackend and
OnnxDroneInferenceBackend (YOLOv8/RT-DETR ONNX decode + NMS via
onnxruntime-node); site-analysis/dem-provider.ts has FixtureDemProvider
and SrtmDemProvider (SRTM GeoTIFF/.hgt via gdal-async, Horn slope/aspect);
the EPA/borehole/NDVI providers follow suit. The fixture backends are fully
implemented and test-covered; the native backends are written but require their
native dependency and model/tile files to be present at deploy time. This is the
honest boundary — the algorithms are real, the heavyweight runtime is pluggable
and not bundled.
Cross-Domain Boundaries#
@cybele/integration holds the bridge contracts to the five sibling domains —
brigid-bridges.ts, saraswati-bridges.ts, asase-bridges.ts,
freya-bridges.ts, maat-bridges.ts. Each defines the typed exchange surface
(e.g. BrigidFactoryAutomationBridge with PrefabProductionOrder/
ProductionStatus and a BridgeHealthCheck) and currently keeps state in
in-memory Maps; the actual network transport to the *_API_URL endpoints is
configuration-level. The directional rules are fixed: Cybele owns land,
property, construction, BIM/design, prefab, real-estate finance, and property
operations; Brigid owns plant control inside facilities Cybele tracks as assets;
Hestia/Annapurna own culinary operations inside Cybele's restaurant buildings;
Freya owns retail/manufacturing business semantics inside Cybele's envelopes;
Maat reads Cybele's financial/risk outputs for governance. A Gaia
weather/climate integration (Phase 175 — asset-hardening alerts, site weather
risk, flood/heat analysis) is referenced by the specs and modelled in the
WeatherCondition type, but there is no gaia-bridges.ts in integration/src
today; treat it as planned rather than wired.
Invariants, Failure Modes, Extension Points#
Invariants. Coordinates always carry their srid (default 4326) and are
never computed against without a CRS. Monetary values are GHS unless a field
names its own currency. Construction schedule/budget changes are auditable via
the append-only hash-chained ledger; corrections never mutate prior events.
Financial projections retain their input assumptions for reproducibility. Lease
and tenant records enforce role-based access and immutable signoff.
@cybele/core has no intra-domain upstream dependencies.
Failure modes. The gateway swallows handler errors into a CYBELE_500
envelope (message hidden in production) and rate-limits at 200 req/min/IP. Kafka
consumer failures are DLQ'd and swallowed to prevent rebalance storms. The
ledger fails loud on tamper (verifyChain). Provider native backends throw if
their binding/model is absent rather than fabricating detections — the fixture
backend is the safe default for tests. PgBouncer transaction-mode pools disable
prepared statements (prepare=false); migrations must use the direct
(buildDirectUrl) non-pooled path for DDL.
Extension points. Add an aggregate by extending @cybele/core types +
validators + guards, a pgTable + migration in @cybele/db, a capability
library, an event in CYBELE_EVENT_TYPES, and (if externally visible) a schema
in @contracts/cybele. Swap a runtime backend via the
repository/ledger/provider seams (setPropertyRepository,
setConstructionLedger, a Fixture… → production provider). Add a cross-domain
link with a new *-bridges.ts file.
Implementation Status#
The domain is an implemented TypeScript workspace with substantial,
genuinely domain-specific logic — MCDA site scoring, CPM/PERT scheduling, Horn
DEM analysis, Ghana mortgage/REIT/pro-forma finance, EN 15978 carbon, secp256k1
construction and title ledgers, and a real kafkajs/Hono/Drizzle/PostGIS stack.
What is seam-level / not live by default: the construction ledger and
property repository ship in-memory defaults behind production-injectable
interfaces; the ONNX/SRTM/EPA provider native backends require their bindings
and model files; the integration bridges define contracts with in-memory state
rather than live HTTP clients; and the Gaia (Phase 175) integration is
spec-referenced, not yet a bridge. Phase 58 (TODOS/phase-58.md) is the active
build-out phase; the db and api sources carry inline 58.1.2.x / 58.1.3.x
subtask anchors marking that work. These seams are honest fail-loud boundaries,
not stubs: they compute and refuse rather than fabricate.