OUT-3844: move realtime connection status to dedicated table - #58
Conversation
Add a secret-free xero_connection_status table (portal_id, status, updated_at) so Supabase Realtime can run under RLS + anon without broadcasting xero_connections.tokenSet. - New schema + migration with an AFTER INSERT OR UPDATE trigger on xero_connections that upserts status into xero_connection_status, guarded by IS DISTINCT FROM to skip token-only writes - One-time backfill seeds status rows for existing portals so their first status change surfaces as a realtime UPDATE - Repoint useRealtimeXeroConnections to the new table - Add supabase/snippets RLS/grant/publication snippet (run manually) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (OUT-3844) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
Greptile SummaryThis PR moves Xero connection status out of the token-bearing
Confidence Score: 5/5Safe to merge — the core token-isolation goal is fully achieved, the migration and hook changes are correct, and the two noted issues are narrow edge cases in a manually-run operational snippet and a first-connection scenario the OAuth redirect already handles. The migration, trigger logic, backfill, Drizzle schema, and hook repoint are all correct and consistent. The two observations are a non-idempotent CREATE POLICY in the manual snippet (trivially fixed with IF NOT EXISTS) and a narrow gap where brand-new portals' first-ever connection fires a realtime INSERT that the UPDATE-only hook doesn't catch — behaviour already present in the original code and largely masked by the OAuth redirect. Neither affects the primary token-security objective of the PR. The manual snippet supabase/snippets/2026-06-10_rls_and_xero_connection_status.sql is the only file worth a second look — specifically the CREATE POLICY idempotency before running in production. Important Files Changed
Sequence DiagramsequenceDiagram
participant App as App Server (Drizzle)
participant XC as xero_connections
participant Trigger as sync_xero_connection_status()
participant XCS as xero_connection_status
participant RT as Supabase Realtime
participant Hook as useRealtimeXeroConnections
App->>XC: INSERT / UPDATE (status, tokenSet, …)
XC->>Trigger: AFTER INSERT OR UPDATE (per row)
alt "TG_OP = INSERT OR status changed"
Trigger->>XCS: INSERT … ON CONFLICT DO UPDATE (portal_id, status, now())
XCS-->>RT: WAL UPDATE event broadcast
RT-->>Hook: "payload { portal_id, status, updated_at }"
Hook->>Hook: compare connectionStatus vs payload.new.status
alt status differs
Hook->>Hook: window.location.replace
else same
Hook->>Hook: skip
end
else token-only write (status unchanged)
Trigger->>Trigger: no-op (IS DISTINCT FROM guard)
end
Reviews (3): Last reviewed commit: "docs(OUT-3844): document RLS, grants & r..." | Re-trigger Greptile |
Create the anon SELECT policy before enabling RLS and wrap the snippet in a transaction so a live subscriber never sees RLS-on-without-policy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/ explaining the anon lockdown: RLS on all public tables, revoke all anon access, grant select only on the xero_connection_status mirror table for Supabase Realtime. Also widen the snippet's REVOKE to all tables in the public schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What & why
Closes OUT-3844.
Supabase Realtime currently subscribes to
xero_connections, whosetokenSetcolumn holds the Xero OAuth access + refresh tokens.postgres_changesbroadcasts the whole row, and RLS is row-level (not column-level), so enabling RLS + anon read access to keep realtime working would still leaktokenSetto any anon client. The hook only ever readsstatus.This moves connection status onto a dedicated, secret-free table so RLS + anon realtime can be enabled without exposing tokens.
Changes
xero_connection_status(portal_idPK,status,updated_at) — schema + migration.xero_connections(AFTER INSERT OR UPDATE) that upsertsstatusinto the new table. Guarded byIS DISTINCT FROMso token-only writes (refreshes) don't emit needless realtime events. Covers all current and future write paths with no app-code changes.UPDATE(the event the hook listens for) rather than a missedINSERT.useRealtimeXeroConnectionsfromxero_connectionstoxero_connection_status.supabase/snippets/2026-06-10_rls_and_xero_connection_status.sql, run manually): enables RLS, grants anonSELECT+ revokes writes, swaps the realtime publication to the new table, and enables RLS onxero_connections(closing anon REST reads).Deployment order (important)
The snippet drops
xero_connectionsfrom the realtime publication, so it must run after the repointed hook is deployed:xero_connection_status).Verification
{ portal_id, status, updated_at }— notokenSet.xero_connectionsreturns[].Testing Criteria
https://www.loom.com/share/11a10eb005a54d82b524e205f135dbeb
🤖 Generated with Claude Code