Skip to content

Private-only doorbell: drop the public fallback, surface failures → 0.2.0-next.26 - #24

Merged
HomemadeToast57 merged 1 commit into
mainfrom
jack/private-only-doorbell
Jul 28, 2026
Merged

Private-only doorbell: drop the public fallback, surface failures → 0.2.0-next.26#24
HomemadeToast57 merged 1 commit into
mainfrom
jack/private-only-doorbell

Conversation

@HomemadeToast57

Copy link
Copy Markdown
Collaborator

Supersedes the compat approach in #21. Nothing real depends on the pre-private doorbell, so the fallback is deleted rather than carried.

Why

The public compat channel cost a second realtime.messages insert on every row change, forever, left an anon-joinable topic protected only by the obscurity of a schema name that ships in every bundle, and had no retirement mechanism. A permanent tax and a permanent leak surface, to serve users who don't exist.

What changes

  • Every doorbell topic is wristband-gated. A socket with no gateway-minted token hears nothing at all.
  • One broadcast per row change instead of two.
  • The id-stripping workaround from next.25 is gone with the channel that needed it (Realtime's injected message-uuid was only ever observable there).
  • Failure is surfaced, not disguised. With no degraded path to slink onto: capped-backoff retry (1s → 5s → 15s → 60s, reset by a good join) plus doorbell.status() / onStatus reporting connecting | live | unauthorized | unavailable. HTTP reads keep working, so an app is never broken — just not live, visibly.
  • mint() returns a discriminated MintResult so a 403 ("you may not watch this app") is distinguishable from a network failure or 404/503. This matters most for public apps, where every visitor is normally admitted and must never be told they're unauthorized because a request dropped.

Public no-auth apps — the primary path

Verified on dev against the real gateway route:

anonymous mint: 200
  topics: {"app":"bool:app_…:app","user":null}   ← nobody signed in
  join: SUBSCRIBED
anonymous viewer got the ROW: true
eavesdropper (anon key, no wristband) on old public topic: heard 0

"Private" means wristband-gated, not login-gated — for a public app the gateway hands one to every visitor.

154 pass, typecheck clean. Pairs with codehs/bool#570.

Nothing real depends on the pre-private doorbell, so the compat path is deleted
rather than carried. It cost a second realtime.messages insert on every row
change in perpetuity, left an anon-joinable topic protected only by the
obscurity of a schema name that ships in every bundle, and had no retirement
mechanism — a permanent tax and a permanent leak surface to serve users who
don't exist.

With no degraded path available, failure has to be honest: the doorbell retries
with capped backoff (1s/5s/15s/60s, reset by a successful join) and reports
connecting | live | unauthorized | unavailable through status()/onStatus. HTTP
reads keep working, so the app still functions — it just isn't live, and says so
instead of silently limping.

mint() returns a discriminated MintResult so a 403 ("you may not watch this
app") is distinguishable from a network failure or a 404/503. That distinction
matters most for public apps, where every visitor is normally admitted and must
never be told they're unauthorized because a request dropped.

Also removes the id-stripping workaround from next.25 — Realtime's injected
message uuid was only ever observable on the public channel, which no longer
exists.

Verified on dev: anonymous visitor of a public no-auth app mints, joins the app
room, and receives full rows; an anon-key eavesdropper on the old public topic
hears nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HomemadeToast57
HomemadeToast57 merged commit ca3b47d 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