Skip to content

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.