From a0d2a4dd9941c4a8cc40f13bd514d620b26ec045 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 10 Jul 2026 22:05:56 +0100 Subject: [PATCH] feat(webapp): make the default realtime backend configurable --- .server-changes/realtime-backend-default-env.md | 6 ++++++ apps/webapp/app/env.server.ts | 2 ++ .../realtime/resolveRealtimeStreamClient.server.ts | 10 +++++----- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .server-changes/realtime-backend-default-env.md diff --git a/.server-changes/realtime-backend-default-env.md b/.server-changes/realtime-backend-default-env.md new file mode 100644 index 0000000000..2f3c98216a --- /dev/null +++ b/.server-changes/realtime-backend-default-env.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Add a `REALTIME_BACKEND_DEFAULT` env var to choose the default realtime backend (`electric`, `native`, or `shadow`) for environments whose org has no per-org override. Defaults to `electric`, so existing behavior is unchanged. diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index cabe0159a6..b91b901313 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -374,6 +374,8 @@ const EnvironmentSchema = z // Master switch for the native realtime backend; off = Electric serves everything, publishes no-op. REALTIME_BACKEND_NATIVE_ENABLED: z.string().default("0"), + // Default backend when an org has no `realtimeBackend` override and no global flag row is set. + REALTIME_BACKEND_DEFAULT: z.enum(["electric", "native", "shadow"]).default("electric"), // Live long-poll backstop hold (ms); matches Electric's ~20s cadence. REALTIME_BACKEND_NATIVE_LIVE_POLL_TIMEOUT_MS: z.coerce.number().int().default(20_000), // Jitter ratio on the live-poll hold (0.15 = ±15%) to avoid synchronized refetch herds. diff --git a/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts b/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts index 4b2207fe19..ac666744e6 100644 --- a/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts +++ b/apps/webapp/app/services/realtime/resolveRealtimeStreamClient.server.ts @@ -13,8 +13,8 @@ import { getShadowRealtimeClient } from "./shadowRealtimeClientInstance.server"; type RealtimeBackend = "electric" | "native" | "shadow"; -// Two gates, both defaulting to the Electric path: the env master switch, then the -// per-org `realtimeBackend` feature flag (cached so long-polls don't hit the DB per request). +// Two gates: the env master switch, then the per-org `realtimeBackend` feature flag (cached so +// long-polls don't hit the DB per request), falling back to REALTIME_BACKEND_DEFAULT. const nativeBackendEnabled = env.REALTIME_BACKEND_NATIVE_ENABLED === "1"; const flag = singleton("realtimeBackendFlag", () => makeFlag($replica)); @@ -61,7 +61,7 @@ async function getRealtimeBackend( return cached; } - let backend: RealtimeBackend = "electric"; + let backend: RealtimeBackend = env.REALTIME_BACKEND_DEFAULT; try { const overrides = @@ -76,7 +76,7 @@ async function getRealtimeBackend( backend = await flag({ key: FEATURE_FLAG.realtimeBackend, - defaultValue: "electric", + defaultValue: env.REALTIME_BACKEND_DEFAULT, overrides: (overrides as Record) ?? {}, }); } catch (error) { @@ -85,7 +85,7 @@ async function getRealtimeBackend( organizationId, error, }); - backend = "electric"; + backend = env.REALTIME_BACKEND_DEFAULT; } backendCache.set(organizationId, backend);