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.