Realtime v2 migration and rollback
Canary Realtime v2 beside an existing adapter, understand the bulk-event change, and roll back without changing schema or losing outbox data.
Realtime v2 keeps the existing client.collections.*.live(), client.globals.*.live(), raw client.realtime, TanStack { realtime: true }, and RealtimeConfig.adapter APIs. The SSE wire remains protocol 1; responses advertise it as X-Questpie-Realtime-Protocol: 1, which existing clients safely ignore.
The rollout flag changes server transport selection only. It does not create, delete, or rewrite a table.
Realtime v2 channels add framework-owned ledger tables, and SSE presence adds questpie_channel_presence. Generate and apply the normal QUESTPIE database migration before enabling channel traffic. Do not hand-edit these tables. The presence table contains expiring connection leases; multiple app instances sharing the application database reconcile the same roster.
The breaking behavior to audit
Bulk update and bulk delete emit one durable event per logical operation, not one event per affected record. A custom RealtimeAdapter or outbox consumer must handle:
{
operation: "bulk_update" | "bulk_delete",
payload: {
count: number,
recordIds: Array<string | number>,
},
}Do not count adapter notifications to count changed records. Use payload.count. Do not expect N per-record events from one bulk mutation.
Rollout modes
realtime.rollout.mode accepts three values:
| Mode | Cross-instance invalidation | Edge delivery | Use it for |
|---|---|---|---|
"legacy" | Existing adapter or the poll/implicit-pg fallback | Existing SSE protocol | Baseline and immediate rollback |
"v2" | changeBroker; an existing adapter remains the compatibility fallback when no broker is configured | Configured clientTransport, otherwise SSE | Normal operation; this is the default |
"dual" | Existing adapter and changeBroker both publish the same durable outbox sequence | One v2 edge path only | Canary comparison without double-delivering client frames |
Dual mode requires both adapter and changeBroker. It compares whether both invalidation publishes accepted the same durable sequence. A one-sided failure is reported through onComparison and logs a warning, while the healthy path continues. If both fail, normal reconciliation polling still provides the durable recovery path and the publish reports failure.
Recommended migration
1. Deploy the new code on the legacy path
Keep the current adapter and pin the baseline explicitly:
export default runtimeConfig({
realtime: {
adapter: existingAdapter,
rollout: { mode: "legacy" },
},
});Verify existing admin, Autopilot, live(), liveIter(), and TanStack realtime consumers. No client release is required.
2. Canary both invalidation paths
Add the v2 broker and compare outcomes. Client snapshots are still emitted once:
export default runtimeConfig({
realtime: {
adapter: existingAdapter,
changeBroker: nextBroker,
rollout: {
mode: "dual",
onComparison(result) {
if (!result.equivalent) {
metrics.increment("realtime.dual_run_mismatch", {
legacy: result.legacy,
v2: result.v2,
});
}
},
},
},
});Run dual mode long enough to cover deploys, reconnects, bulk mutations, and a broker interruption. The durable outbox remains the source of truth; dual mode compares invalidation acceptance, not application row payloads.
3. Select v2
After the canary is clean:
export default runtimeConfig({
realtime: {
changeBroker: nextBroker,
clientTransport: nextClientTransport,
rollout: { mode: "v2" },
},
});An app that only has adapter can also use the default v2 mode. QUESTPIE maps that adapter onto the new invalidation/compute/delivery pipeline as a compatibility fallback, so existing configuration does not need a flag-day rewrite.
Rollback drill
Rollback is a config change:
- Set
realtime.rollout.modeto"legacy". - Keep the existing
adapterconfigured. - Redeploy or restart every instance.
- Confirm
POST /realtimereturns protocol header1and a knownlive()consumer receives a fresh snapshot. - Confirm outbox lag returns to normal through the adapter or reconciliation poll.
Do not roll back migrations or delete questpie_realtime_log, channel-ledger rows, or questpie_channel_presence. Both modes consume the same durable data, so changing the flag loses neither schema nor committed events. Presence lease rows expire or are removed by the runtime. If the adapter itself is unavailable, leave reconciliation polling enabled while restoring it.
Compatibility checklist
- Existing
realtime: trueworks. - Existing
realtime: { adapter }works without adding a broker. - Existing SSE clients remain on protocol
1. - Dual mode never sends two snapshot streams to one client.
- Custom adapter consumers accept consolidated bulk events.
- The rollback drill succeeds without a down migration or data deletion.
Realtime adapter
Configure the durable realtime runtime, its notice broker, SSE or Pusher/Soketi edge delivery, reconciliation, admission limits, privacy, and deployment-specific drivers.
Storage adapter
Point your file uploads at the local disk, S3, R2, or any of 40+ Files SDK backends with one config block, and get a typed Files instance on app.storage, automatic signed URLs for private files, and direct-to-storage uploads, all without touching your collections.