firebase / firebase/firebase-tools
Allow @refresh(onMutationExecuted:) to fire across connectors within the same Data Connect service
- Dominant language
- TypeScript
- Stars
- 4.5k
- Forks
- 1.3k
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 84
Description
Currently, the @refresh(onMutationExecuted: { operation: "OpName" }) directive only fires when the named operation is executed against the same connector that declared the directive. Operations with matching names in other connectors of the same Data Connect service do not trigger the refresh, even though all connectors share the same underlying Postgres instance and table set.
Use case:
Our product is a multi-app platform built on a single Data Connect service with multiple connectors targeting one Cloud SQL Postgres database:
- mobile app
- manager web
- admin web portal
- cloud functions (admin)
These surfaces all read and write the same underlying tables through their own connector's mutations. Realtime subscriptions on the app should reflect changes made by users in the web version, or triggered flows in cloud functions to support multi-stakeholder workflow our users need.
Today, a user subscribed to a query in app with @refresh(onMutationExecuted: { operation: "UpdateXStatus" }) does NOT receive a push when a user runs UpdateXStatus against web, even though that mutation writes the exact row the user is observing. This breaks the most valuable real-time UX scenarios. Cross-role propagation that is motivating us to adopt subscriptions in the first place.
Verified behavior (tested with current SDK and CLI):
- Connector A declares @refresh(onMutationExecuted: { operation: "FooOp" }).
- Connector A's FooOp triggers the push ✓
- Connector B's FooOp does NOT trigger the push, even though both mutations write the same underlying Postgres row ✗
Requested behavior:
@refresh(onMutationExecuted: ...) should fire for any matching operation across all connectors in the same Data Connect service when the conditions are met. The matching surface is the operation name (or, ideally, the underlying table writes), not the connector boundary. It could even be connector::operation syntax too.
If a fully-implicit cross-connector default would be too aggressive a behavior change, an opt-in API would also be valuable, for example:
# Option 1: implicit scope flag (default could remain current connector for backwards compat)
@refresh(onMutationExecuted: {
operation: "UpdateXStatus",
scope: SERVICE # vs current default CONNECTOR
})
# Option 2: explicit cross-connector reference
@refresh(onMutationExecuted: {
connector: "web",
operation: "UpdateXStatus",
condition: "mutation.variables.claimId == request.variables.claimId"
})
# Option 3: refresh on row-level writes regardless of mutation name
@refresh(onTableWrite: {
table: "C",
condition: "mutation.row.id == request.variables.claimId"
})
Option 3 is the most powerful, table-level refresh would obviate the need to enumerate every mutation across every connector that might touch a row, which is also useful for native SQL mutations and custom resolvers that bypass the
_upsert/_update/_delete operation names.
Current workaround (and why it's insufficient):
We are working around the limitation by generating Dart SDKs for each cross-mutating connector and opening additional WebSocket connections from the app to subscribe to mirror queries in each consumer connector. This works functionally but has significant costs:
1. N WebSocket connections per client (one per consumer connector subscribed). On mobile, this means more battery drain, more reconnect surface, more concurrent auth contexts.
2. Mirror queries become a maintenance liability. Each cross-connector flow requires a parallel query declaration in the consumer connector with matching shape, auth, and @refresh directives. Drift between mirrors and the canonical query is silent when a developer adds a field to one and forgets the other, stale data appears in production with no error.
3. Cross-connector auth surface expansion. Mirror queries in connectors like web need @auth rules that allow reads expanding the auth surface of connectors that were originally scoped to a single audience.
4. Bundle bloat. Each consumer connector's full Dart SDK ships in the adjuster app even though the user only consumes a small read-only slice.
5. Doesn't help mutations from NO_ACCESS admin connectors / Cloud Functions — those write via *-admin connectors that don't expose subscribable queries to clients at all. Server-originated changes can't propagate to clients without yet another mechanism (e.g., bridging via Event Triggers + Cloud Functions doing redundant writes).
Contributor guide
Assessment
This issue has not been assessed yet.