TLDR: When PostgREST has two paths between two tables, it picks neither — and one of those failures looks exactly like "no data."

The Setup

The KOL CRM for an ecommerce business (a Supabase-backed relationship manager for key opinion leaders) started with a clean schema: webinars had a direct topic_id FK and a direct kol_id FK.

Then we shipped 013_multi_kol_topic.sql, which added webinar_kols and webinar_topics junction tables alongside those legacy columns.

Simple enough, right? Right.

The Wall

We were running a totally unrelated migration — audit table, RLS policies, some RPCs. Nothing about webinars or topics.

It forced a PostgREST schema-cache reload. And that reload weaponized every latent embed ambiguity simultaneously.

Three places broke. Two were loud. One was terrifying.

The loud one: /analytics 500'd behind an error boundary. Fine, easy to catch.

The quiet one: the KOL detail page broke, but only when you actually navigated to an activity list. Annoying, still catchable.

Then there was the silent one.

src/queries/supply-chain.ts destructured the Promise.all result like this:

const [{ data: upcomingWebinars }] = await Promise.all([...])

No .error check. So when PostgREST returned PGRST201, the error just… evaporated. The dashboard rendered "Upcoming Webinars: 0" with zero visual indication anything was wrong. Clean UI, completely wrong data, five scheduled webinars invisible.

That is the bug that keeps me up at night.

What I Tried First (That Didn't Work)

Auto-resolution. The embeds had been working fine before the migration. I trusted that PostgREST would keep auto-resolving. It did — until it didn't. Schema-cache reloads re-derive all relationships from scratch and enforce ambiguity it had previously skimmed past.

!inner as a fix. I'd seen !inner in some PostgREST examples and thought it might help. It doesn't. !inner only changes the join type to an inner join — it does NOT disambiguate between two relationships. In nested embeds it can fail silently. Completely wrong tool.

The Fix That Worked

PostgREST embed hints. Three forms:

table!fk_constraint_name(...)   # e.g. webinars!webinars_topic_id_fkey(...)
table!column_name(...)          # e.g. kol_details!contact_id(...)

For 1:1 extension tables like kol_details, use the FK column name: !contact_id. Proven pattern from the web app's src/queries/*.ts.

For tables with a legacy direct FK and a junction, use the constraint name: !webinars_topic_id_fkey. Postgres names these <table>_<column>_fkey by convention — you can confirm with \d webinars in psql.

The iOS app (supabase-swift, our SDK for the mobile client) takes the exact same select string. Copy it from the web app's queries. No translation needed.

The Sweep Pattern

After any migration on a project with junction+direct-FK pairs, I now run:

grep -rn ':topics\|:contacts\|:traffic_sources' src/ | grep -v '_fkey'

Any hit without a _fkey hint is a latent time bomb.

Why This Matters

The loud PGRST201 error is fine — it tells you exactly which tables are fighting. Quote the table names, find the matching hint in your web app's queries, paste it.

The silent failure is the real lesson: never swallow .error inside a Promise.all destructure. If a query can return nothing and your UI just shows zero, you will not catch this in QA. You will catch it when a meeting starts and someone asks why there are no webinars on the board.

One schema-cache reload. Five invisible webinars. Full-table sweep every migration from now on.