Skip to content

Strip legacy-channel payloads to {table, op} → 0.2.0-next.25 - #21

Merged
HomemadeToast57 merged 1 commit into
mainfrom
jack/legacy-payload-sanitize
Jul 28, 2026
Merged

Strip legacy-channel payloads to {table, op} → 0.2.0-next.25#21
HomemadeToast57 merged 1 commit into
mainfrom
jack/legacy-payload-sanitize

Conversation

@HomemadeToast57

Copy link
Copy Markdown
Collaborator

Supabase Realtime injects its own message-uuid id into any broadcast payload that lacks one. The public compat channel is row-data-free by contract, so once the platform stopped putting a row id there (codehs/bool#570 — it was leaking primary keys on an anon-joinable channel), subscribers began receiving Realtime's uuid instead — indistinguishable from a row id to LiveEntityStore, which keyed-fetched a nonexistent row, applied nothing, and silently swallowed the change.

Found by verifying the platform fix end-to-end on dev:

public payload: {"id":"29dac8e9-…","op":"INSERT","table":"idprobe"}
row id inserted: 11111111-2222-3333-4444-555555555555
payload.id === row id?  false   ← so it's a message id, not a row id

The doorbell now strips legacy-channel payloads to {table, op} instead of trusting the transport's shape. Absent id is exactly the signal that makes the store fall back to a coalesced full reload — correct behavior for a channel with no row data. Private payloads are untouched (they keep id and row).

Only reachable on the fallback paths — no mint desk (older platform), revoked access, refused join — which is also where it was hardest to notice.

2 new regression tests (legacy stripped / private preserved). 152 pass, typecheck clean.

Supabase Realtime injects its own message-uuid `id` into any broadcast payload
that lacks one. The public compat channel is row-data-free by contract, so after
the platform stopped putting a row id there (it was leaking primary keys on an
anon-joinable channel), subscribers started receiving Realtime's uuid instead —
indistinguishable from a row id to the live store, which then keyed-fetched a
row that doesn't exist, applied nothing, and silently swallowed the change.

Discovered by verifying the platform-side fix end-to-end on dev: the public
payload still had an `id`, and it did not match the inserted row's.

The doorbell now discards `id`/`row` on the legacy channel rather than trusting
the transport's payload shape. A missing id is precisely the signal that makes
LiveEntityStore fall back to a coalesced full reload — correct for a channel
that carries no row data. Private payloads keep both fields.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HomemadeToast57
HomemadeToast57 merged commit 73cdcc0 into main Jul 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant