Architecture

Last generated 2026-07-13 · window 2026-07-12T18:02:00Z–2026-07-13T18:01:03Z

┌─────────────────────────────────────────────────────────────┐
│  Browser (Inertia + React)                                   │
│  Session auth · CSRF · corporation context                   │
└──────────────────────────┬──────────────────────────────────┘
                           │ /api/v1/*  /admin/*  /revenue-management/*
┌──────────────────────────▼──────────────────────────────────┐
│  apps/web (Laravel)                                          │
│  Pennant flags · Horizon queues · Passport/Sanctum           │
└─┬────────────┬──────────────┬──────────────┬────────────────┘
  │            │              │              │
  ▼            ▼              ▼              ▼
MySQL      ClickHouse     Redis         Reverb
(KPI opt)  (KPI store)    (cache/queue) (widget chat)
  │
  ├── data-service (Go) ── POST /api/fetch
  ├── events-service (Go) ── browse events → ClickHouse
  ├── rev-mgmt-engine (Go) ── Layer 1/2 pricing runs
  └── agent-runtime (TS) ── widget chat SSE/WS + managed-agents turns
           │               (RapidStor MCP tools for booking)
           ▼
    RapidStor API (external) · SiteLink (via importers)

Services #

ServicePathRole
Web appapps/web/Primary Laravel application, all user-facing routes
Data serviceapps/data-service/KPI fetch API for dashboards (single-corp)
Events serviceapps/events-service/Ingests browse/analytics events
Rev-mgmt engineapps/rev-mgmt-engine/Pricing engine runs, talks to RapidStor overlay API
Agent runtimeapps/agent-runtime/Standalone TypeScript service for managed-agents conversation domain, streaming, and AI-credit metering; shares @r6digital/agent-contract wire vocabulary with Laravel

Application modularity #

Each Applications enum case maps to a Pennant feature scoped per corporation. Middleware check.application.flag gates Inertia pages; API routes use complementary checks. Non-essential apps default to disabled until explicitly enabled.

Sidebar visibility is built by ApplicationMenuBuilder, which delegates flag state-math to ApplicationStateResolver. Application metadata (label, link, icon, parent/child relationships) is declared via Applications::definition() returning an AppDefinition value object.

ApplicationFlags persisted integers are non-monotonic (COMING_SOON=4 above ENABLED=3; INTERNAL=1 below DISABLED=2), so raw min() over enum values is not a correct availability ceiling. ApplicationFlags::availability() provides a derived rank (HIDDEN=0, DISABLED/COMING_SOON=1, INTERNAL=2, ENABLED=3) used by effectiveState() to clamp corp overrides against global. INTERNAL is a global-only admin gate: a global INTERNAL stays INTERNAL for every corp (unless explicitly HIDDEN); corps cannot assert INTERNAL themselves (stored corp INTERNAL coerces to DISABLED).

Parent containers (e.g. Revenue Management) appear when the parent is not HIDDEN and has at least one visible child. Visibility checks use "not HIDDEN" rather than numeric enum ordering.

Revenue Management — entitlement signal #

RM visibility is decoupled from the legacy revenue_management visibility flag. A first-class per-corp Pennant bool revenue_management_entitled (default false) governs whether a corporation is provisioned on the engine. Entitled corps resolve RM nav/routes to ENABLED; un-entitled corps see the COMING_SOON teaser. Global HIDDEN still acts as a hard kill-switch. Global INTERNAL wins over the entitlement gate: an un-entitled corp with global INTERNAL projects to INTERNAL (staff preview) rather than COMING_SOON; the engine gate remains ENABLED-only so pilots never run live pricing.

Admin enable/onboard actions set revenue_management_entitled (not the visibility flag). Engine-connect, JWT claims, cron, and ensure.rev-mgmt.flag middleware all read entitlement via RevMgmtFlagResolver. RM child apps (rev_man_*) are entitlement-driven — corp-scope visibility writes are ignored; the admin grid renders them read-only in corporation mode.

Revenue Management — Home & Portfolio data plane #

Nav item REV_MAN_HOME links to GET /revenue-management/home/{facility?} (Inertia). Portfolio stays gated on the same Pennant key via AppDefinition.relatedRoutes (revenue-management.portfolio). Both pages hydrate from the engine GET /api/portfolio-page/:facility bundle (proxied by Suite).

Home sections: HubCards (Approval Centre / Rules / Portfolio / Reporting coming-soon), AttentionStrip (corp-wide pending + below-market counts from bulk-pricing), KPI/ROI strip (occupancy + RevPAF live; Value Created / ROI gated on hasValueReport), OperationsSection (health + occupancy trend), FacilitiesTable (per-facility occupancy, RevPAF, opportunity, churn, auto-apply, connectivity), ActivityFeed, and ValueReportSection (attribution when engine emits value_report). Missing engine fields render not-wired placeholders — never fabricated zeros.

Engine portfolio bundle readers (corp-scoped, enabled facilities only): facilities table from site_units inventory + pending opportunity; health aggregates (sites count, occupancy, MRR delta, RevPAF); occupancy trend from ledger history; value_report from realised rec uplift (pushRates + ecri) over trailing 12 months UTC.

RapidStor Settings (native screen) #

Schema-driven settings UI at /rapidstor-settings/{site?} replaces the legacy AngularJS admin panel for operators when the global rapidstor_internal_settings Pennant flag is ON (or INTERNAL for admins only). SettingsSchema defines ~380 fields across tabs; SettingsService reads/writes via SettingsClient, which authenticates with the corporation's rs_api_token at RapidStor POST /api/v1/authenticate (cached JWT, not a shared signing secret).

RapidStor exposes server-to-server internal endpoints for Automate: GET/PUT /api/v1/internal/settings/site (per-site deep-merge of rapidstor/fees/hours/administrative/skin/friendlyName + gating block for tab visibility) and GET/PUT /api/v1/internal/settings/corp (corp-level Section B settings). Corp cache is busted and re-warmed synchronously on write so booking flows see new settings immediately.

Tenant Journey — translation overrides #

Operators can override booking-journey copy at language, corporation, and location scopes. JourneyTranslationResolver merges layers for the public API; the Journey Editor supports click-to-translate against a live RapidStor v2 preview iframe (VITE_RAPIDSTOR_FRONTEND_URL). RapidStor mono consumes overrides via GET /api/v1/tenant-journey/translations.

Scheduled translations:sync pulls the RapidStor v2 frontend's bundled translation tree from its public JSON endpoint (/api/translations Page Function) and upserts base languages + corp overrides into Automate. Short-circuits when payload hash is unchanged. Complements the one-shot import-frontend-translations.mjs dev script.

Revenue Management — unit-type identity #

The rev-mgmt engine (apps/rev-mgmt-engine/) keys Mongo writes (revManRateOverrides, revManActiveExplorations), Redis engineCache invalidation, and SiteLink rate pushes on the RapidStor unit-type ObjectId (_id), not the reconstructed <w>x<l>-<feature> code string. ADR-0010: multiple distinct priced groups can share one code (e.g. 34 CHAH unit-types collapsing to 16 codes); the _id is the authoritative identity.

  • Pending recommendations carry unit_type_id (RapidStor hex) stamped by the engine at emit time. ListPendingCorp and approve flows prefer this field.
  • Approve service (internal/approve/service.go): objectIDForRec uses rec.UnitTypeID when present; falls back to ResolveObjectID (code → _id lookup in MySQL rev_mgmt_unit_type_families) only for legacy un-onboarded recs with empty _id.
  • Layer 1 & Layer 2 runs set Recommendation.UnitTypeID from UnitType.RapidstorUnitTypeID and derive deterministic recommendation IDs from the _id (code fallback when un-onboarded).
  • Unit expansion: UnitsInType expands a unit-type group _id to SiteLink unit IDs so collision-code siblings are never swept into the same override.

Known limitation: ClickHouse rev_mgmt_evaluation_windows is keyed on unit_type_code (no _id column), so Layer 2 recs with colliding codes still share pooled-per-code pricing until a future migration.

Revenue Management — unit-type discriminator (ADR-0010 Stage-2) #

Because unit_type_code is a non-unique label (multiple RapidStor unit-types at one facility can share a code), the Approval Centre grid keys rows on _id but displays a human discriminator beneath the code: Type · Floor N · Features. Discriminator fields (type_name, sitelink_unit_type_id, floor, features) are persisted in rev_mgmt_unit_type_families (engine migration 0023 + Suite mirror) and refreshed on every onboard from the widened resolver payload. Legacy rows without discriminator data fall back to code-only display.

Managed Agents — runtime extraction #

The managed-agents conversation runtime is being extracted from apps/web/app/Services/Ai/* into apps/agent-runtime/ (TypeScript). Phase 0–2 is live: contract package, turn loop, replay, MCP client, agent profiles, and a public widget API at /widget/* (embed-token auth, CORS-scoped by allowedOrigins). The RapidStor mono chat widget now talks directly to this service (SSE on send, watch WebSocket for operator/takeover) instead of Laravel Reverb. Laravel still owns in-app assistant UI and admin console; the runtime service handles widget streaming turns and calls RapidStor MCP tools (enabled_locations__v1, unit_list_by_type__v1, reservation/enquiry). Wire contract lives in packages/agent-contract. Production deploys via docker-swarm.yml at agents.r6automate.com; local dev via Lando at agent-runtime.automate.lndo.site.

Cross-product: Tenant Journey and Revenue Management integrate with RapidStor via configured API tokens and webhooks. Rev-mgmt reads unit types via RapidStor internal API (/api/v1/internal/unit-types) and writes overlays back to per-corp Mongo. See RapidStor docs for the internal API surface.