Rada v2 / Supabase Collapse
Rada is the weather-and-alert side of the Atram system: historically a set of sibling services (rada-weather, the rada-message-system "Composer", and alert-manager "MeteOps") backed by their own Supabase Postgres database, kept entirely separate from Yaya Engine's own Postgres [@business]. "Rada v2" is the name for the project that collapses that separate Supabase database into Yaya Engine, so Yaya Engine becomes the single system of record for geography, forecasts, alerts, and dispatch state instead of one of two databases that have to stay in sync [@business]. The "collapse" matters because, before it, downstream Rada consumers read Supabase directly (via PostgREST) and inferred facts — like which FSP a user belongs to — from brittle heuristics, instead of asking Yaya Engine for an answer it actually owns.
Why the split existed, and why it is a problem¶
Supabase's users table is populated by a one-way sync from Yaya Engine: each
Yaya Engine user creates a mirrored row in Supabase users and
alert_subscriptions, with yaya_user_id set when a Supabase row has a
matching Yaya Engine record [@business]. Supabase never got an fsp_id
column, so alert-manager and rada-message-system have had to infer FSP from
country plus whether yaya_user_id is null — a heuristic that breaks the
moment a country gets a second FSP [@business]. BUSINESS.md's stated interim
fix, ahead of the full collapse, is to add an fsp_id mirror column to
Supabase users directly, so those consumers can stop guessing [@business].
The Rada v2 project is the more complete fix: instead of adding more mirror
columns to Supabase for every new dimension, move the data itself into
Yaya Engine and let Rada services query it there.
What "cutover" means concretely¶
The cutover has two visible surfaces in this repo. The first is schema: a run
of Rada v2 migrations creates countries, seasons, events,
event_fsp_assignments, town_forecasts, town_observations,
forecast_configs, and dispatch_modes (plus their audit tables) directly in
Yaya Engine's own Postgres — tables that previously would only have existed in
Supabase. A verification pass on 2026-07-08 confirmed all 113 migrations in
the schema (Rada v2's plus everything pre-existing) apply cleanly on a real
Postgres server, that every Rada v2 foreign-key child column is covered by a
leading-column index, and that migrate:rollback tears them back down with no
errors [@pg-verification]. That verification also caught, as an
out-of-scope side finding, ~21 pre-existing (non-Rada-v2) foreign-key columns
with no covering index — a separate hygiene migration was written to add
those indexes, but it is not part of the Rada v2 schema itself
[@pg-verification].
The second surface is the read path. config/rada.php defines
RADA_USER_IDENTITY_SOURCE, an environment toggle with two values:
supabase (the historical behavior — read user-to-town mappings from the
legacy Supabase mirror over PostgREST, a cross-database call) and
yaya_engine (read locally from Yaya Engine's own user_nearest_towns table)
[@rada-config]. It defaults to supabase so that deploying the code that
supports the new path is a no-op until an environment explicitly flips the
flag [@rada-config]. This is the general shape of the whole cutover: land the
Yaya-Engine-native code and data path first, flip a flag per environment, and
only remove the Supabase path once nothing is left calling it.
The tripwire: proving the old path is really dead¶
Because the cutover happens gradually and across several external services
(rada-weather, the Composer, alert-manager), Yaya Engine cannot just delete
its Supabase client and hope nothing breaks. Instead, every Saloon request
that goes out through App\Http\Integrations\Supabase\SupabaseConnector
logs a structured rada.supabase_call line on a dedicated rada log channel,
tagged with the resolved endpoint, HTTP method, and the calling class/method
[@tripwire]. In production this channel forwards to Laravel Nightwatch, so
the expected post-cutover steady state — zero calls — is something that can
actually be watched for, with alert thresholds of "healthy" at 0/5 minutes,
"warn" above 5 calls a minute, and "page" above 50 a minute [@tripwire]. Any
rada.supabase_call event firing after cutover is a regression: a forgotten
sync path, most likely App\Services\SupabaseService (gated behind
services.supabase.sync_enabled), App\Services\Messaging\BroadcastService,
or App\Http\Controllers\Api\V1\UsersWithForecastsController, which is
documented as a read path still awaiting migration to a Yaya-Engine-local
query [@tripwire]. This tripwire is the mechanism that turns "we think we cut
over" into something verifiable in logs, rather than an assumption.
The replacement contract: /api/v1¶
Once data lives in Yaya Engine's own Postgres, Rada services need a way to
read and write it that isn't a raw database connection. That contract is the
/api/v1 route surface: token-authenticated route groups such as
api.v1.scheduled-alerts.*, api.v1.composed-messages.*,
api.v1.dispatch-batches.*, api.v1.users.*, and an /api/v1/geography
group, each one built to back a specific consumer migration off Supabase
[@api-routes]. Each route block in routes/api.php documents which epic and
issue it belongs to and links to a matching doc under docs/api/v1/
[@api-routes] — see API v1 surface for the
full contract. The architecture page
Supabase and Rada v2 covers the
legacy Supabase integration and the tripwire in more depth, and
Rada v2 Postgres cutover covers the
decision record for moving the schema onto Postgres. Anyone extending Rada
v2 schema — Yaya Engine's or Rada's — should treat this concept as the reason
new geography, forecast, or dispatch tables belong in Yaya Engine's own
migrations rather than a fresh Supabase column.