Skip to content

feat: add row-set subscriptions (ids: [UUID!]) and RLS-aware rowId masking#1108

Merged
pyramation merged 2 commits intomainfrom
feat/realtime-sparse-set
May 10, 2026
Merged

feat: add row-set subscriptions (ids: [UUID!]) and RLS-aware rowId masking#1108
pyramation merged 2 commits intomainfrom
feat/realtime-sparse-set

Conversation

@pyramation
Copy link
Copy Markdown
Contributor

@pyramation pyramation commented May 10, 2026

Summary

Extends graphile-realtime-subscriptions with two features:

  1. Row-set filtering (ids: [UUID!]) — subscribers can pass an array of row IDs and only receive NOTIFY events whose row IDs intersect with that set. Events with no matching IDs are silently dropped before re-query.

  2. RLS-aware rowId masking — updated docstrings and type definitions to clarify that rowId is masked (set to null) when resource.get() returns null due to RLS denial, preventing metadata leaks.

The subscription field signature changes from:

onXxxChanged(id: UUID): XxxSubscriptionPayload

to:

onXxxChanged(ids: [UUID!]): XxxSubscriptionPayload

Two subscription modes are supported:

  • Specific rowsonXxxChanged(ids: ["uuid-a", "uuid-b"]) — pass a single-element array for one row
  • Full collectiononXxxChanged (no args) — subscribe to any change on the table

The previous id: UUID single-record argument was removed; a single-element ids array covers that case without the redundant API surface.

Companion PR: constructive-io/constructive-db#1098 adds architecture documentation for the full realtime flow.

Review & Testing Checklist for Human

  • Verify Grafast tuple unpacking at runtime: The lambda([$payload, $ids], (pair) => ...) pattern assumes pair is a tuple [raw, subscribedIds]. This is tested with mocks that return callback results directly — verify this matches actual Grafast behavior when the plan executes with a real pgSubscriber.
  • Null vs empty array handling: subscribedIds can be null, undefined, or []. The guard subscribedIds && subscribedIds.length > 0 handles all three, but confirm this matches the GraphQL argument semantics (does omitting ids yield null or undefined from args.get('ids')?).
  • Breaking change for existing id: consumers: The id: UUID argument is removed. If any existing subscription queries use id:, they will fail at the GraphQL layer. Confirm no consumers exist yet (this plugin has not been wired into the server preset, so this should be safe).

Suggested test plan: Wire the plugin into a PostGraphile server with a @realtime-tagged table, open a WebSocket subscription with ids: [...], and verify that NOTIFY events for non-subscribed rows are filtered out while subscribed rows are delivered.

Notes

  • All 45 unit tests pass (10 new tests added covering sparse set filtering, rowId resolution with ids, and RLS masking documentation)
  • TypeScript compilation passes with --noEmit
  • The rowId field description in the generated schema now explicitly states it is "masked when RLS denies access"
  • The row field resolver no longer has a subscribedId (singular) code path — all filtering goes through subscribedIds (array)

Link to Devin session: https://app.devin.ai/sessions/19485cf5cc58416a9f86068563d512f5
Requested by: @pyramation

… masking

- Add ids: [UUID!] argument for sparse set subscription mode
- Filter NOTIFY events by row ID intersection with subscribed set
- Mask rowId (set to null) when RLS denies access to prevent metadata leaks
- Support three subscription modes: single record, sparse set, full collection
- Add 11 new tests covering sparse set filtering and RLS-aware delivery
- Update docstrings to document security model and subscription modes
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Remove redundant id: UUID argument — a single-element array covers the
single-record case. This simplifies the API surface to just two modes:
specific rows (ids) and full collection (no args).
@devin-ai-integration devin-ai-integration Bot changed the title feat: add sparse set subscriptions (ids: [UUID!]) and RLS-aware rowId masking feat: add row-set subscriptions (ids: [UUID!]) and RLS-aware rowId masking May 10, 2026
@pyramation pyramation merged commit 691db9b into main May 10, 2026
54 checks passed
@pyramation pyramation deleted the feat/realtime-sparse-set branch May 10, 2026 22:40
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