Domain · Architecture

Cybele Domain — Architecture

Cybele is the Oshun bounded context for the built environment.

10sections11 minread1diagrams

On this page

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, and apps/cybele/*. (Scope tag scope:cybele; API port 4027; database schema cybele; 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:

text
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-flavoured ai-selection.ts.
  • design — structural, MEP, sustainability, BIM (bim.ts), drawings, and generative.ts (parametric massing/unit-mix optimisation under setback/plot- ratio constraints) plus a BIM-to-digital-twin.ts pipeline.
  • 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-building iot.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 cryptographic blockchain-title.ts registry (Ethereum-style Wallet, keccak256, EIP-191, secp256k1 ECDSA over node: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.

flowchart TD subgraph apps["apps/cybele/* (Hono services)"] A_API[api / gateway]:::app A_PORT[portfolio]:::app A_PROJ[projects]:::app A_MKT[marketplace]:::app A_FAC[factory]:::app end subgraph gw["@cybele/api gateway runtime"] REST["REST routers: properties · construction · leases · materials"] GQL[GraphQL schema] GRPC[5 gRPC service defs] WS[WebSocket channels] LEDGER[construction ledger] KAFKA[Kafka publisher / consumer] end subgraph cap["capability libraries (domain algorithms)"] SITE[site-analysis] & DES[design] & CON[construction] PROP[property] & FIN[finance] & MAT[materials] & PT[proptech] end CORE["@cybele/core (types · validators · guards)"]:::core DB["@cybele/db (Drizzle + PostGIS · PgBouncer · Redis cfg)"]:::core CONTRACTS["@contracts/cybele (Zod schemas · 31 CloudEvents)"]:::core INTEG["@cybele/integration bridges"] apps --> gw gw --> cap cap --> CORE cap --> DB gw --> CORE KAFKA -->|CloudEvents 1.0| CONTRACTS INTEG -->|approved facts| EXT["Brigid · Saraswati · Asase · Freya · Maat"] cap --> INTEG classDef app fill:#1f6feb22,stroke:#1f6feb; classDef core fill:#2da44e22,stroke:#2da44e;

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 groupspropertiesRouter, 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.tstypeDefs + 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.