Domain · Architecture

Freya Domain - Architecture

The single most important thing to understand before reading further: every Freya capability library and application is a deterministic engine that operates over in-process Map-backed stores.

8sections11 minread1diagrams

On this page

Luxury Goods and Fashion Intelligence operating system (libs/freya/*, apps/freya/*, libs/contracts/freya; TODO Phase 57). An implemented in-process TypeScript domain: deterministic business engines today, with a persistence and transport surface defined as code but not yet wired to live infrastructure.

Freya is the bounded context for luxury-goods businesses operating in African fashion, beauty, textiles, jewelry, and accessories. It models the operating system a brand or conglomerate needs to create products, source materials, manufacture at scale, sell through multiple channels, and analyze financial performance — from a designer's first concept board through to a customer's purchase and the brand's per-business-unit P&L. It is intentionally supply-side: Freya runs the business of making and selling luxury goods. It does not make per-consumer styling decisions, and it does not own physical real estate; those concerns belong to Aglaea and Cybele respectively.

Freya sits in the application/domain tier of the Oshun monorepo, not the engine tier. Unlike Neith (a sovereign Rust runtime kernel that everything builds on), Freya is a TypeScript workspace that builds on shared platform contracts and publishes domain state outward. Its principal downstream consumer is Aglaea, the consumer-styling domain, which reads Freya's products, sizing, inventory, campaigns, and provenance to drive recommendation experiences. The dependency arrow points one way: no Freya library imports from Aglaea.

What "Implemented" Means Here#

The single most important thing to understand before reading further: every Freya capability library and application is a deterministic engine that operates over in-process Map-backed stores. The domain logic — state machines, costing, AQL inspection, relevance scoring, formulation validation — is real, specific, and fully implemented. The infrastructure around it (PostgreSQL, Redis, MinIO, Kafka, gRPC) is defined as typed schema and configuration code but is not actually connected. Concretely:

  • apps/freya/api/src/order-routes.ts keeps orders in const orders = new Map<string, OrderResponse>() and exposes resetOrderStore() for test isolation — there is no database call in the request path.
  • No capability library or application imports @freya/db. A repo-wide grep shows @freya/db is referenced only by its own tests. The 24-table Drizzle schema is a parallel, currently-unconsumed persistence definition, not the backing store for the running code.
  • apps/freya/api/src/event-bus.ts defines the CloudEvents envelope, the event taxonomy, the Kafka topic-routing map, and buildFreyaEventBusConfig(), but instantiates no Kafka producer.
  • apps/freya/api/src/grpc-services.ts says so in its own header comment: "Defines Protobuf-style service contracts as TypeScript interfaces. Actual .proto files are generated from these definitions." It ships generateProtoStub(), not a running gRPC server.

This is an honest, fail-loud architecture, not a faked one: the engines compute real results from real inputs; the persistence/transport layer is a complete contract waiting for a wiring phase. The sections below label each layer's status explicitly.

Workspace Shape#

Freya occupies three areas of the monorepo: twenty-six libraries under libs/freya/* (two foundation, twenty-four capability), a shared contracts package at libs/contracts/freya, and five applications under apps/freya/*.

text
libs/freya/
  core/   db/                      # foundation (everyone depends on core)
  fashion/   textiles/   beauty/   # multi-module capability libraries
  brand/     ecommerce/  manufacturing/
  retail/    academy/    jewelry/
  market-intel/  financials/  connectors/  sota/
  watches/   perfumes/   bridal-events/    # single-module facade libraries
  hair-care/ home-decor/ footwear/
  eyewear/   luggage-leather/  personal-care/
  cleaning-products/  textile-finishing/

libs/contracts/freya/              # @freya/contracts — shared Zod schemas
apps/freya/
  api/  studio/  shop/  retail/  manufacturing/

Architectural Layers#

Foundation: @freya/core (implemented)#

@freya/core (libs/freya/core/src/index.ts) is the type-and-algorithm foundation every other Freya package imports — dozens of source files across the workspace depend on it. It is in-process TypeScript types and pure functions, not database rows. Its modules are product-types.ts, material-types.ts, supply-chain-types.ts, brand-customer-types.ts, beauty-types.ts, business-units.ts, and utilities.ts.

The foundation is where Freya's domain correctness actually lives, and the algorithms are specific rather than generic CRUD:

  • Branded identity. Six template-literal ID types (FreyaProductId = FPRD-…, FreyaOrderId = FORD-…, etc.) make the compiler reject a FreyaOrderId where a FreyaProductId is expected. Generators build IDs as PREFIX{hexTimestamp}-{6-hex-sequence}-{4-hex-random} with a monotonic sequence wrapping at 0x1000000.
  • Quality / supply chain (supply-chain-types.ts): computeAQLSampleSize returns standard sample sizes per lot band; evaluateQualityResult returns fail on any critical defect, conditional_pass when minors exceed twice the AQL threshold, pass otherwise; computeSupplierScore is the weighted aggregate performance×0.4 + onTime×0.3 + qualityPassRate×0.3.
  • Pricing (product-types.ts): getPriceForMarket resolves a tier, prefers the requested currency, and falls back via a fixed market→currency map.
  • Beauty chemistry (beauty-types.ts): validateINCISum enforces ingredient percentages summing to 100% ±0.5; computeFragranceAccordStrength weights base notes ×1.5, heart ×1.2, top ×1.0.
  • Utilities (utilities.ts): real EAN-13/UPC-A check-digit algorithms, fixed EXCHANGE_RATES_TO_GHS, textile unit conversions, a Ghana Kente/Ankara Pantone palette with nearest-color matching, and the FreyaError hierarchy (FreyaValidationError, FreyaNotFoundError, FreyaBusinessRuleError) used for fail-loud guards across every library.

The Business-Unit Catalog and Its Facade Libraries#

business-units.ts exports FREYA_BUSINESS_UNIT_CATALOG, a frozen array of nineteen FreyaBusinessUnitProfile records (code, packageName, displayName, summary, primaryCategories, operatingCapabilities, productionModels, commercialChannels, keyMetrics). assertCompleteFreyaBusinessUnitCatalog enforces exactly nineteen entries with unique codes and packages.

Honest correction to features.md: eleven of the capability libraries — watches, perfumes, bridal-events, hair-care, home-decor, footwear, eyewear, luggage-leather, personal-care, cleaning-products, and textile-finishing — are not domain-rich engines. Each is an 18-to-20-line facade that reads its profile from the catalog. libs/freya/watches/src/index.ts in its entirety exposes getWatchesBusinessUnitProfile(), hasWatchesOperatingCapability(capability), and listWatchesKeyMetrics() over getFreyaBusinessUnitProfile('watches'). These are honest, useful registry accessors — they fabricate nothing — but the operational logic for those units lives in the multi-module libraries: fragrance and haircare in @freya/beauty (fragrance-development.ts, haircare-development.ts); personal-care and detergent in @freya/beauty (personal-care-detergent.ts); finishing in @freya/textiles (finishing-operations.ts); and assembled goods (footwear, eyewear, leather) in @freya/manufacturing (assembly-manufacturing.ts).

Capability Libraries (multi-module, implemented)#

Thirteen libraries carry the bulk of the domain logic, each a set of focused modules over in-memory state. Every one depends only on @freya/core. A few representative, code-grounded examples:

  • @freya/fashion (design-pipeline.ts, collection-planner.ts, size-grading.ts, production-scheduler.ts, trend-forecasting.ts, african-print-library.ts). DesignPipeline is a guarded finite state machine over concept → mood_board → sketch → pattern → prototype → sample → production (with discontinued reachable from any stage and an ALLOWED_TRANSITIONS table that throws on illegal moves). DesignCostEstimator computes garment cost from Standard Minute Value: laborCost = (SMV/60) × rate, overhead = labor × 0.30. PrototypingTracker caps revision cycles at five.
  • @freya/ecommerce (catalog-inventory.ts, diaspora-commerce.ts, social-personalization.ts). InventoryService.computeStockLevel returns max(0, warehouseStock − activeReservations), treating non-committed, non-expired reservations as held; reserveStock/commitReservation/ releaseReservation implement a TTL soft-lock with multi-warehouse draw-down (Accra first). OrderManagementService.transitionStatus enforces a fourteen-state DTC machine. SearchEngine.computeRelevanceScore is the documented blend text×0.4 + popularity×0.3 + recency×0.2 + margin×0.1.
  • @freya/financials (unit-economics.ts, financial-planning.ts, startup-investment.ts). GarmentUnitEconomics carries named constants (TRIMS_PCT_OF_FABRIC = 0.08, OVERHEAD_PCT_OF_CMT = 0.30, WHOLESALE_MULTIPLIER = 2.2, RETAIL_MULTIPLIER = 2.4) and computes a COGS breakdown plus wholesale/retail ladders.
  • Plus @freya/textiles, @freya/beauty, @freya/manufacturing, @freya/retail, @freya/brand, @freya/jewelry, @freya/academy, and @freya/market-intel, each following the same pattern: domain-specific classes and pure functions over Map stores, guarded by FreyaBusinessRuleError/FreyaValidationError.

@freya/connectors (implemented as brief-builders; not wired)#

@freya/connectors (index.ts re-exporting asase-, brigid-, cybele-, saraswati-, maat-connectors.ts) is the intended single point of cross-domain integration. As built, it computes hand-off artifacts in-process rather than calling the other domains. For example cybele-connectors.ts::FactoryConstruction.buildFactorySpec(productionType, capacity, workforce) derives floor area (workforce×8 + capacity×0.5 m²), power (workforce×2 + capacity×0.1 kVA), clean-room class (ISO 8 for cosmetics), and compliance standards (adding ISO 22716 GMP for cosmetics, OEKO-TEX/GOTS for textiles); FlagshipStoreDesign.computeStoreFloorArea sizes a flagship from peak traffic and dwell time. These are real luxury-retail/industrial heuristics that produce a request payload — but there is no import of, or network call to, Cybele/Brigid/Asase/Saraswati/Maat. The connector is the correct seam; live dispatch across it is roadmap.

@freya/sota (implemented enrichment seam)#

@freya/sota (ai-design.ts, ai-analytics.ts, computer-vision.ts, nlp.ts, blockchain-auth.ts, virtual-tryon.ts) houses the opt-in state-of-the-art enrichment layer. It is deliberately separated from the deterministic capability libraries so business logic stays testable and predictable even when an AI enrichment is unavailable or returns an unexpected result.

@freya/db (defined-as-code; unconsumed)#

@freya/db (schema.ts, connection.ts, redis-config.ts, s3-config.ts, observability.ts, seed.ts) is a complete persistence specification: twenty-four freya_* Drizzle tables (grep -c pgTable = 24) with UUID PKs, jsonb columns, and four vector(1536) pgvector embedding columns; buildFreyaDbPoolConfig sizing pools per service role; FreyaRedisKeys/ FreyaRedisTTL/FreyaPubSubChannels; three MinIO/S3 bucket policies; and an OpenTelemetry/Prometheus/circuit-breaker config. It is real, careful schema and config — but as noted above, nothing in the running domain imports it. The DB integration test (integration.spec.ts) gates its live-Postgres assertions behind skipIf(process.env.FREYA_DATABASE_URL === undefined) and otherwise exercises pure logic (S3 key construction, INCI validation, Redis channel naming), so the suite passes without infrastructure.

@freya/contracts (implemented)#

libs/contracts/freya/src/index.ts re-exports four Zod schema modules (product-schemas, order-schemas, customer-schemas, manufacturing-schemas) that form the stable, validated API boundary. These are consumed by the API app's handlers and are the schemas any other Oshun domain would use to call Freya.

Applications (implemented over in-memory state)#

Five apps compose the libraries into workflows. They depend on capability libraries but never on each other. apps/freya/api is the surface (schemas.ts, middleware.ts, the *-routes.ts handlers, graphql-schema.ts, websocket.ts, grpc-services.ts, event-bus.ts, openapi-spec.ts). studio, shop, retail, and manufacturing are the design, DTC, store-ops, and factory-floor apps. The API enforces real RBAC in middleware.ts (ROLE_PERMISSIONS for customer/associate/manager/admin, :own ownership checks, three rate-limit buckets) and publishes a genuine OpenAPI 3.1.0 document (FREYA_OPENAPI_SPEC in openapi-spec.ts, with components, paths, and $ref-linked schemas).

Component and Data Flow#

flowchart TD subgraph Apps["apps/freya/* (in-memory Map stores)"] Studio[studio] Shop[shop] Retail[retail-app] Mfg[manufacturing-app] API[api: REST + RBAC + OpenAPI] end subgraph Caps["libs/freya/* capability libraries"] Fashion[fashion] Ecom[ecommerce] Manu[manufacturing] RetailLib[retail] Beauty[beauty] Textiles[textiles] Brand[brand] Jewelry[jewelry] Fin[financials] Market[market-intel] Sota[sota] Facades[11 BU facade libs] end subgraph Foundation["foundation"] Core["@freya/core: types + algorithms"] Catalog["FREYA_BUSINESS_UNIT_CATALOG"] end subgraph Defined["defined-as-code, NOT wired"] DB["@freya/db: 24 Drizzle tables + Redis/S3 cfg"] Bus["event-bus: CloudEvents taxonomy + topic map"] Grpc["grpc-services: contracts + .proto generator"] Conn["connectors: cross-domain brief builders"] end Studio --> Fashion & Beauty & Textiles & Brand Shop --> Ecom Retail --> RetailLib Mfg --> Manu API --> Ecom & Manu & Fin & Market Caps --> Core Facades --> Catalog Catalog --- Core Sota -. opt-in enrichment .-> Caps Caps -. would persist via .-> DB API -. would emit via .-> Bus API -. would call .-> Grpc Caps -. would dispatch via .-> Conn Conn -. roadmap .-> Aglaea[(Aglaea & peer domains)]

The intended operational cycle, much of which is implemented in-process today:

  1. Creation. Designers/operators create product, material, collection, and manufacturing records (studio + API). Objects get branded IDs and enter the product/design lifecycle state machines.
  2. Enrichment. brand, market-intel, and sota attach trend, campaign, demand, and pricing signals. (sota is opt-in; the deterministic core stands alone without it.)
  3. Selling. ecommerce and retail publish only Active products and run carts, checkout, payments, and omnichannel inventory against a shared stock model.
  4. Manufacturing and quality. manufacturing consumes orders and closes the production loop with status updates and AQL-gated inspection; a failed inspection holds the order until rework re-passes.
  5. Planning. financials and market-intel aggregate into per-unit P&L, brand health, and supplier-risk views that feed the next design cycle.

Three Order State Machines (a deliberate, real distinction)#

Freya models orders at three different surfaces, each with its own lifecycle — this is intentional, and a frequent source of confusion:

  • Supply-chain OrderStatus (@freya/core/supply-chain-types.ts): draft → placed → confirmed → in_production → quality_check → ready → shipped → delivered, with cancelled. This is the manufacturing/procurement order.
  • DTC fulfillment (@freya/ecommerce/catalog-inventory.ts): a fourteen-state machine placed → payment_confirmed → processing → picked → packed → shipped → out_for_delivery → delivered, plus return/refund states.
  • API customer order (apps/freya/api/order-routes.ts): a compact PLACED → CONFIRMED → SHIPPED → DELIVERED → RETURNED, CANCELLED from PLACED/CONFIRMED. The handler validates transitions against a validTransitions table and rejects illegal moves.

These are not duplicates to be merged; they describe genuinely different processes (procuring stock, fulfilling a parcel, tracking a customer order).

Cross-Domain Boundaries#

  • Freya vs. Aglaea. Supply-side vs. consumer-side. Freya publishes products, sizing, inventory, campaigns, and provenance; Aglaea consumes them. Freya never makes a per-consumer styling decision; no Freya library imports Aglaea.
  • Freya vs. Cybele. Physical stores and factories are Cybele's; Freya models the business operations inside them. The Cybele connector builds store-design and factory-construction briefs.
  • Freya vs. Brigid. Factory-automation machinery is Brigid's; Freya models execution and inspection at the batch/order level.
  • Freya vs. Saraswati. Logistics and IoT for smart/tech products are Saraswati's; Freya consumes them when a product embeds devices.
  • Freya vs. Maat / Asase. Freya produces brand/market/financial analytics that Maat aggregates; Freya consumes Asase for natural-ingredient traceability. All five integrations route through @freya/connectors.

Today these boundaries are honored structurally (the connector is the only place that would reach across), but the connectors compute briefs locally rather than calling the peer domains.

Invariants, Failure Modes, and Extension Points#

Invariants. Capability libraries depend only on @freya/core, never on each other (verified: the libraries import @freya/core and not sibling capability packages). The business-unit catalog must hold exactly nineteen unique entries (assertCompleteFreyaBusinessUnitCatalog). Fiber and INCI compositions must sum to 100% ±0.5. Product/material provenance is meant to be append-only. State machines reject illegal transitions by throwing rather than silently coercing.

Failure modes. Guards fail loud with typed FreyaError subclasses (FREYA_VALIDATION_ERROR, FREYA_NOT_FOUND, FREYA_BUSINESS_RULE_VIOLATION) rather than returning sentinels. The largest systemic gap is durability: every store is an in-process Map, so state is lost on restart and is not shared across processes — resetOrderStore()-style helpers exist precisely because state is in-memory. Idempotency (a stated commerce requirement) is partially modeled via reservation tokens and TTL soft-locks but is not yet enforced through a transactional database.

Extension points. Adding a business unit means appending a profile to FREYA_BUSINESS_UNIT_CATALOG, bumping the assertion, and adding a facade (or, for a rich unit, a multi-module library) over @freya/core. New events are added to the FreyaEventType union and routed in FREYA_EVENT_TOPICS. New gRPC methods are appended to FREYA_GRPC_SERVICES and flow through generateProtoStub. The clearest forward path for the domain is the wiring phase: making the capability libraries import @freya/db, persisting through the existing Drizzle schema, and standing up the already-specified Kafka/gRPC/Redis surfaces.

Verification Expectations#

Each library and app ships a Vitest suite (*.spec.ts). Because state machines, inventory accounting, AQL inspection, and financial calculations are the domain's correctness core, their tests assert specific computed values against known-correct answers, not just shape. Commerce, payment, inventory, manufacturing, and customer-data changes additionally warrant contract (@freya/contracts) and schema (@freya/db) regression coverage so the typed boundary and the persistence definition stay consistent — even while the running engines remain in-memory. Run with pnpm nx test <project> (or npx vitest run directly from a library directory when Nx is contended by worktrees).