Skip to content

fix(trailbase-db-collection): handle 403 on subscribe/* with polling fallback#1521

Open
royalcala wants to merge 1 commit intoTanStack:mainfrom
royalcala:fix/trailbase-polling-mode-subscribe-403
Open

fix(trailbase-db-collection): handle 403 on subscribe/* with polling fallback#1521
royalcala wants to merge 1 commit intoTanStack:mainfrom
royalcala:fix/trailbase-polling-mode-subscribe-403

Conversation

@royalcala
Copy link
Copy Markdown

Problem

When a TrailBase table has row-level access rules (_ROW_.*), the server cannot evaluate those predicates for wildcard subscriptions and returns 403 Forbidden on GET /api/records/v1/<table>/subscribe/*.

This caused two separate bugs:

Bug 1 — Collection stuck in loading state forever

start() called subscribe("*") without any error handling. The thrown 403 error propagated past the finally block, so markReady() was never calleduseLiveQuery.data remained undefined indefinitely, even though list requests returned data normally.

Bug 2 — Every optimistic mutation rolled back after 120 s

onInsert, onUpdate, and onDelete called awaitIds() unconditionally. awaitIds waits for the subscription SSE stream to confirm the server has persisted the record by checking seenIds. In polling-only mode seenIds is never populated (no SSE events arrive), so the wait always timed out → TimeoutWaitingForIdsErrorthe optimistic mutation was rolled back.

Fix

  1. Wrap subscribe("*") in try/catch — log a console.debug message and continue without SSE when the subscribe call fails (e.g. 403).
  2. Hoist sseAvailable flag to closure scope — set to true only when SSE connects successfully. This allows the three mutation handlers to check it.
  3. markReady() is always called via finally — the collection becomes usable even without live updates.
  4. onInsert/onUpdate/onDelete skip awaitIds() when !sseAvailable — the collection reflects server state after the next polling cycle instead of timing out.
  5. periodicCleanupTask is only started when SSE is actually available — avoids a no-op interval running forever in polling-only mode.

Behaviour after fix

  • Tables with open subscribe/* access → unchanged behaviour (SSE + awaitIds as before).
  • Tables with row-level access rules that deny subscribe/* → polling-only mode: data loads via list requests, optimistic mutations persist immediately and are confirmed after the next poll.

Repro

// TrailBase config.textproto — row-level rule denies wildcard subscribe
read_access_rule: "_ROW_.user_id = _USER_.id"
  • Open the app → collections stuck loading (useLiveQuery.data === undefined)
  • Insert a record → appears briefly, then disappears after ~2 min (TimeoutWaitingForIdsError)

…fallback

When a TrailBase table has row-level access rules (_ROW_.*), the server
cannot evaluate them for wildcard subscriptions and returns 403 Forbidden.

Previously start() called subscribe("*") without error handling, so the
thrown error prevented markReady() from being called — leaving the
collection in a permanent loading state where useLiveQuery.data is
always undefined.

Additionally, onInsert/onUpdate/onDelete called awaitIds() unconditionally,
which waits for SSE events to confirm persistence. In polling-only mode
seenIds is never populated, so awaitIds would time out after 120 s and
reject, rolling back every optimistic mutation.

Fix:
- Wrap subscribe("*") in try/catch; log a debug message and fall back to
  polling when it fails (e.g. 403).
- Hoist sseAvailable flag to closure scope; set it to true only when SSE
  connects successfully.
- markReady() is now always called via finally, so the collection becomes
  usable even without live updates.
  collection reflects server state after the next polling cycle instead.
- periodicCleanupTask is only started when SSE is actually available.
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