Skip to content

Domain Data Model

Yaya Engine's schema has grown across 114 migrations, from the original FSP and user tables through a long tail of messaging, alerts, and verification tables, to a "Rada v2" era in late June and July 2026 that added geography, seasons/events, and dispatch tables directly into this database [@migrations-dir]. Reading the Eloquent models rather than the migrations is the faster way to understand the current shape: they group naturally into user/auth, FSP, messaging/conversations, alerts/weather, and actions/verifications/payouts, with the Rada v2 geography and dispatch models forming a newer, largely self-contained sixth group. All of it hangs off two central models — User and Fsp — and a single FSP/country scoping pattern that shows up as a reusable trait rather than a tenancy package.

User and auth: the center of the graph

User is the busiest model in the codebase: it belongs to an Fsp, and has many MagicLink, UserLocation (plus a primaryLocation hasOne scoped to is_primary), ChannelAccount, Conversation, Message, UserAlert (and an activeAlert hasOne scoped to is_active), UserAction, RequestedPayout, MarketingFlowExecution, and PushSubscription [@user-model]. A User::booted() hook runs inheritCountryFromFsp() on creation, defaulting the user's country column from their FSP's country whenever it is not explicitly set — the comment on that method explains why this matters: self-serve onboarding through Telegram or OTP never collects a country directly, and without the fallback the column stays null, which breaks both the Supabase alert-subscription sync and the Yaya Manager country filter [@user-model]. A separate AdminUser model, not User, backs staff and machine auth (see Auth architecture); it is a deliberately separate table so that granting an admin permission never touches the end-user table. Around User sit the pieces of the actual auth flows: MagicLink (an 8-character token, a single_use flag, and an expires_at column) [@magic-link-model] and OtpCode, both detailed in Auth architecture, plus UserLocation and UserNearestTown, which together replace what an older architecture note describes as a single lat/lng pair stored directly on users — that plan was superseded by a proper user_locations table with a primaryLocation relation, which is what User::hasLocation() and hasConfirmedLocation() actually query [@user-model].

FSP: the branding and configuration boundary

Fsp has many users, branches (FspBranch), and alertConfigs (FspAlertConfig), and has one whatsappConfig, smsConfig, and marketingFlow [@fsp-model]. This is the model that makes Yaya's white-label distribution work: an FSP's frontend_url, logo_path, primary_color/secondary_color, and loan_offering_* fields drive what a given FSP's users actually see in the PWA, while whatsappConfig/smsConfig let each FSP route outbound messages through its own channel credentials rather than a single shared one. See FSP as a Multi-Tenancy Concept for why FSP is a first-class concept here rather than an incidental foreign key.

Scoping without a tenancy package

Nothing in this schema uses a multi-tenancy package — FSP and country scoping is implemented as a plain Eloquent scope, duplicated deliberately across models rather than centralized in a base class. User::scopeFilteredByManagerSession() reads manager_filter_country/manager_filter_fsp out of the session (the same session keys ManagerGlobalFilters middleware writes, see HTTP route surfaces map) and narrows a query by country/fsp_id directly [@user-model]. Models that do not belong directly to a user reuse the same idea through a trait, FiltersByManagerSession, which offers two scopes: scopeFilteredByManagerSession() for models with a direct user() relation, and scopeFilteredByManagerSessionVia($relation) for models that reach a user through a nested relation, such as a UserActionStep reaching a user through userAction.user [@filters-by-manager-session]. Any new model that the Yaya Manager needs to filter by country or FSP is expected to pull in this trait rather than reinvent the session-reading logic.

Messaging, alerts, and the action/payout lifecycle

Three more clusters round out the core domain. Messaging runs ChannelAccount (a user's identity on a channel) into Conversation (per-user, per-channel) into Message (with delivery tracked separately in MessageDeliveryLog), with MessageBatch grouping outbound sends for reporting — this pipeline is covered in depth in Messaging Pipeline: Channel Routing and the Inbound/Outbound Job Chain. Alerts run Alert (hazard type, severity, event date) into UserAlert (the per-user assignment) into UserAction (a user's instance of an Action, itself a preparation-plan template with ordered ActionSteps) into UserActionStep, which is where evidence gets uploaded and reviewed [@action-model] [@alert-model]. Action additionally implements a localization pattern — its booted() hook calls syncLocalizedAttributes() on save, keeping name, description, behavior_statement, and requirements synced into paired *_i18n JSON columns so the same action template can render in a user's preferred locale [@action-model]. UserActionStep records approvedBy/rejectedBy against AdminUser, and a completed UserAction can produce a RequestedPayout, which itself records processedBy against AdminUser — this is the verification-and-payout loop that Preparation Plans: Actions, Verifications, and Payouts explains from the product side, and that the Yaya Manager's validation and payments pages exist to operate (see Yaya Manager Inertia Ops Console).

Rada v2: geography and dispatch tables added directly to this schema

The most recent migrations — from 2026_06_24 through 2026_07_13 — add a materially different kind of table: towns/states geography, countries/seasons/events (a Season → Event → Alerts hierarchy scoped to country rather than FSP), scheduled_alerts/composed_messages/dispatch_batches for the alert-dispatch pipeline, town_forecasts/town_observations for weather-model data, forecast_configs and dispatch_modes for per-country operational settings, plus their audit tables [@migrations-dir]. These back the /api/v1 endpoints that replaced direct Supabase RPC calls for sibling Rada services (see HTTP route surfaces map and api/v1 Endpoint Reference), and the full story of why this data moved into Yaya Engine's own Postgres instead of staying in the shared Supabase database is in Supabase Integration and the Rada v2 Cutover.