Skip to content

OUT-3844: move realtime connection status to dedicated table - #58

Merged
SandipBajracharya merged 3 commits into
mainfrom
OUT-3844
Jun 12, 2026
Merged

OUT-3844: move realtime connection status to dedicated table#58
SandipBajracharya merged 3 commits into
mainfrom
OUT-3844

Conversation

@SandipBajracharya

@SandipBajracharya SandipBajracharya commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What & why

Closes OUT-3844.

Supabase Realtime currently subscribes to xero_connections, whose tokenSet column holds the Xero OAuth access + refresh tokens. postgres_changes broadcasts the whole row, and RLS is row-level (not column-level), so enabling RLS + anon read access to keep realtime working would still leak tokenSet to any anon client. The hook only ever reads status.

This moves connection status onto a dedicated, secret-free table so RLS + anon realtime can be enabled without exposing tokens.

Changes

  • New table xero_connection_status (portal_id PK, status, updated_at) — schema + migration.
  • Sync via DB trigger on xero_connections (AFTER INSERT OR UPDATE) that upserts status into the new table. Guarded by IS DISTINCT FROM so token-only writes (refreshes) don't emit needless realtime events. Covers all current and future write paths with no app-code changes.
  • One-time backfill seeds status rows for existing portals, so their first status change surfaces as a realtime UPDATE (the event the hook listens for) rather than a missed INSERT.
  • Repoint useRealtimeXeroConnections from xero_connections to xero_connection_status.
  • RLS snippet (supabase/snippets/2026-06-10_rls_and_xero_connection_status.sql, run manually): enables RLS, grants anon SELECT + revokes writes, swaps the realtime publication to the new table, and enables RLS on xero_connections (closing anon REST reads).

Deployment order (important)

The snippet drops xero_connections from the realtime publication, so it must run after the repointed hook is deployed:

  1. Apply the migration (table + trigger + backfill).
  2. Deploy this branch (browser now subscribes to xero_connection_status).
  3. Run the RLS snippet in Supabase.

Verification

  • Realtime payload carries only { portal_id, status, updated_at } — no tokenSet.
  • Connect / disconnect still triggers the redirect.
  • Anon REST read of xero_connections returns [].

Testing Criteria

https://www.loom.com/share/11a10eb005a54d82b524e205f135dbeb

🤖 Generated with Claude Code

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>
@linear-code

linear-code Bot commented Jun 10, 2026

Copy link
Copy Markdown

OUT-3844

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
xero-integration Ready Ready Preview, Comment Jun 12, 2026 9:39am

Request Review

@supabase

supabase Bot commented Jun 10, 2026

Copy link
Copy Markdown

Updates to Preview Branch (OUT-3844) ↗︎

Deployments Status Updated
Database Fri, 12 Jun 2026 09:39:34 UTC
Services Fri, 12 Jun 2026 09:39:34 UTC
APIs Fri, 12 Jun 2026 09:39:34 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Fri, 12 Jun 2026 09:39:34 UTC
Migrations Fri, 12 Jun 2026 09:39:34 UTC
Seeding Fri, 12 Jun 2026 09:39:34 UTC
Edge Functions Fri, 12 Jun 2026 09:39:35 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@SandipBajracharya SandipBajracharya changed the title feat(OUT-3844): move realtime connection status to dedicated table OUT-3844: move realtime connection status to dedicated table Jun 10, 2026
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR moves Xero connection status out of the token-bearing xero_connections table and into a dedicated, secret-free xero_connection_status mirror, allowing Supabase Realtime to broadcast status changes to the anon role without ever exposing OAuth tokens.

  • New xero_connection_status table (portal_id PK, status, updated_at) populated by an AFTER INSERT OR UPDATE trigger on xero_connections, guarded by IS DISTINCT FROM so token-refresh writes don't generate spurious realtime noise; a one-time backfill seeds rows for all existing portals.
  • useRealtimeXeroConnections repointed from xero_connections to xero_connection_status with the matching XeroConnectionStatus type.
  • Manual RLS snippet enables RLS on all public tables, grants anon SELECT-only on the mirror, creates the allow-all policy before enabling RLS to prevent a default-deny gap, and swaps the supabase_realtime publication to the new table.

Confidence Score: 5/5

Safe 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

Filename Overview
src/db/migrations/20260610095713_add_xero_connection_status.sql Creates xero_connection_status table, trigger function with correct INSERT/UPDATE short-circuit, and backfill; backfill only covers pre-migration portals so first-connection INSERTs for new portals won't be caught by the UPDATE-only hook.
supabase/snippets/2026-06-10_rls_and_xero_connection_status.sql Manually-run RLS/publication snippet; policy is correctly created before the RLS DO-loop to avoid default-deny gap, but CREATE POLICY is not idempotent and would fail on a re-run, rolling back the entire transaction.
src/features/auth/hooks/useRealtimeXeroConnections.ts Cleanly repoints the realtime subscription from xero_connections to xero_connection_status with correct type and filter; no logic changes.
src/db/schema/xeroConnectionStatus.schema.ts New Drizzle schema matching the migration DDL; exports both the table object and an inferred Zod type, consistent with other schemas in the project.
src/db/schema/index.ts Exports the new xeroConnectionStatus schema alongside the existing schemas; no issues.
src/db/migrations/meta/_journal.json Appends the new migration entry with correct idx, version, and tag; consistent with the existing journal format.
docs/rls-and-realtime-connection-status.md Clear documentation of the security design, deployment order, and what each snippet step does; accurately reflects the implementation.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (3): Last reviewed commit: "docs(OUT-3844): document RLS, grants & r..." | Re-trigger Greptile

Comment thread supabase/snippets/2026-06-10_rls_and_xero_connection_status.sql Outdated
Comment thread src/db/migrations/20260610095713_add_xero_connection_status.sql
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>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai

@priosshrsth priosshrsth left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

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>
@SandipBajracharya

Copy link
Copy Markdown
Collaborator Author

@greptileai

@SandipBajracharya
SandipBajracharya merged commit 099f37e into main Jun 12, 2026
6 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.

2 participants