diff --git a/web-common/src/features/exports/ExportMenu.svelte b/web-common/src/features/exports/ExportMenu.svelte
index e3dcd52499e..bc494dffbda 100644
--- a/web-common/src/features/exports/ExportMenu.svelte
+++ b/web-common/src/features/exports/ExportMenu.svelte
@@ -5,7 +5,8 @@
import Export from "@rilldata/web-common/components/icons/Export.svelte";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte";
- import { featureFlags } from "@rilldata/web-common/features/feature-flags";
+ import { adminServer } from "@rilldata/web-common/features/app-flags";
+ import { useFeatureFlags } from "@rilldata/web-common/features/feature-flags";
import {
createQueryServiceExportMutation,
V1ExportFormat,
@@ -41,7 +42,7 @@
}
const exportDash = createQueryServiceExportMutation(runtimeClient);
- const { reports, adminServer, exportHeader } = featureFlags;
+ const { reports, exportHeader } = useFeatureFlags();
async function handleExport(options: {
format: V1ExportFormat;
diff --git a/web-common/src/features/feature-flags.ts b/web-common/src/features/feature-flags.ts
index 02e4dcc2a83..6e4dc0be301 100644
--- a/web-common/src/features/feature-flags.ts
+++ b/web-common/src/features/feature-flags.ts
@@ -1,137 +1,87 @@
-import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
-import { writable } from "svelte/store";
+import { useQueryClient } from "@tanstack/svelte-query";
+import { derived, type Readable } from "svelte/store";
import {
createRuntimeServiceGetInstance,
runtimeServiceGetInstance,
type V1InstanceFeatureFlags,
} from "../runtime-client";
import type { RuntimeClient } from "../runtime-client/v2";
-
-class FeatureFlag {
- private _internal = false;
- private _default: boolean;
- private state = writable(false);
- subscribe = this.state.subscribe;
-
- constructor(scope: "user" | "rill", defaultValue: boolean) {
- this._internal = scope === "rill";
- this._default = defaultValue;
- this.set(defaultValue);
- }
-
- get internalOnly() {
- return this._internal;
- }
-
- get defaultValue() {
- return this._default;
- }
-
- toggle = () => this.state.update((n) => !n);
- set = (n: boolean) => this.state.set(n);
- resetToDefault = () => this.set(this._default);
-}
-
-type FeatureFlagKey = keyof Omit
;
-
-class FeatureFlags {
- ready: Promise;
- private _resolveReady!: () => void;
-
- adminServer = new FeatureFlag("rill", false);
- readOnly = new FeatureFlag("rill", false);
- // Until we figure out a good way to test managed github we need to use the legacy archive method.
- // Right now this is true only in an E2E environment.
- legacyArchiveDeploy = new FeatureFlag(
- "rill",
- !!import.meta.env.VITE_PLAYWRIGHT_TEST,
+import { useRuntimeClient } from "../runtime-client/v2/context";
+
+// ── Runtime feature flags (per-deployment) ─────────────────────────────
+//
+// These flags are hydrated from `instance.featureFlags` (sourced from the
+// project's `rill.yaml`). Each `` owns a `RuntimeClient`,
+// and `useFeatureFlags()` derives reactive stores from a `GetInstance` query
+// against that client. Different runtimes have independent flag values, and
+// switching runtimes (e.g. branch swap) updates flags without page refresh.
+//
+// Defaults below act as a fallback when a key is missing from the response.
+// Source-of-truth defaults live in `runtime/drivers/registry.go`.
+
+const RUNTIME_FLAG_DEFAULTS = {
+ ai: !import.meta.env.VITE_PLAYWRIGHT_TEST,
+ exports: true,
+ cloudDataViewer: false,
+ dimensionSearch: false,
+ twoTieredNavigation: false,
+ rillTime: true,
+ hidePublicUrl: false,
+ exportHeader: false,
+ alerts: true,
+ reports: true,
+ chat: true,
+ dashboardChat: false,
+ developerChat: false,
+ deploy: true,
+ stickyDashboardState: false,
+ cloudEditing: false,
+ customCharts: false,
+} as const satisfies Record;
+
+export type RuntimeFeatureFlagKey = keyof typeof RUNTIME_FLAG_DEFAULTS;
+
+export type RuntimeFeatureFlags = {
+ [K in RuntimeFeatureFlagKey]: Readable;
+};
+
+const RUNTIME_FLAG_KEYS = Object.keys(
+ RUNTIME_FLAG_DEFAULTS,
+) as RuntimeFeatureFlagKey[];
+
+/**
+ * Returns reactive stores for the runtime feature flags of the nearest
+ * `` ancestor's `RuntimeClient`. Must be called during
+ * component initialization (top-level `
diff --git a/web-common/src/features/models/workspace/ModelWorkspaceCTAs.svelte b/web-common/src/features/models/workspace/ModelWorkspaceCTAs.svelte
index a7707aacb36..40362802f17 100644
--- a/web-common/src/features/models/workspace/ModelWorkspaceCTAs.svelte
+++ b/web-common/src/features/models/workspace/ModelWorkspaceCTAs.svelte
@@ -13,6 +13,7 @@
import { useRuntimeClient } from "../../../runtime-client/v2";
import { useGetMetricsViewsForModel } from "../../dashboards/selectors";
import ExportMenu from "../../exports/ExportMenu.svelte";
+ import { useFeatureFlags } from "../../feature-flags";
import { useCreateMetricsViewFromTableUIAction } from "../../metrics-views/ai-generation/generateMetricsView";
import NavigateOrDropdown from "../../metrics-views/NavigateOrDropdown.svelte";
import ModelRefreshButton from "../incremental/ModelRefreshButton.svelte";
@@ -26,6 +27,7 @@
export let connector: string;
const runtimeClient = useRuntimeClient();
+ const { ai } = useFeatureFlags();
$: ({ instanceId } = runtimeClient);
$: isModelIdle =
@@ -45,6 +47,7 @@
false,
BehaviourEventMedium.Menu,
MetricsEventSpace.LeftPanel,
+ $ai,
);
diff --git a/web-common/src/features/project/RemoteProjectManager.svelte b/web-common/src/features/project/RemoteProjectManager.svelte
index c3d04e9132b..d057b9b6806 100644
--- a/web-common/src/features/project/RemoteProjectManager.svelte
+++ b/web-common/src/features/project/RemoteProjectManager.svelte
@@ -1,6 +1,7 @@
-
-
-
+{#if $deploy}
+
+
+
+{/if}
diff --git a/web-common/src/features/project/deploy/route-utils.ts b/web-common/src/features/project/deploy/route-utils.ts
index b5ee1875fd0..85c8fc2af04 100644
--- a/web-common/src/features/project/deploy/route-utils.ts
+++ b/web-common/src/features/project/deploy/route-utils.ts
@@ -10,7 +10,7 @@ import type { Page } from "@sveltejs/kit";
import { derived, readable } from "svelte/store";
import { getLocalGitRepoStatus } from "../selectors";
import { page } from "$app/stores";
-import { featureFlags } from "../../feature-flags";
+import { legacyArchiveDeploy } from "../../app-flags";
import type { V1ResourceName } from "@rilldata/web-common/runtime-client";
/**
@@ -113,13 +113,13 @@ export function getDeployingPageUrl(frontendUrl: string, isInvite: boolean) {
*/
export function getDeployOrGithubRouteGetter() {
return derived(
- [createLocalServiceGitStatus(), featureFlags.legacyArchiveDeploy],
- ([$gitStatus, legacyArchiveDeploy]) => {
+ [createLocalServiceGitStatus(), legacyArchiveDeploy],
+ ([$gitStatus, $legacyArchiveDeploy]) => {
const hasLocalGitRepo = Boolean(
$gitStatus.data?.githubUrl && !$gitStatus.data?.managedGit,
);
// For E2E we cannot use github just yet. So do not show the git path for that case.
- const shouldUseGit = !legacyArchiveDeploy && hasLocalGitRepo;
+ const shouldUseGit = !$legacyArchiveDeploy && hasLocalGitRepo;
return {
isLoading: $gitStatus.isPending,
getter: shouldUseGit
diff --git a/web-common/src/features/sample-data/GenerateSampleData.svelte b/web-common/src/features/sample-data/GenerateSampleData.svelte
index 67821f63abf..64626870db9 100644
--- a/web-common/src/features/sample-data/GenerateSampleData.svelte
+++ b/web-common/src/features/sample-data/GenerateSampleData.svelte
@@ -8,14 +8,14 @@
import { object, string } from "yup";
import IconButton from "../../components/button/IconButton.svelte";
import SendIcon from "@rilldata/web-common/components/icons/SendIcon.svelte";
- import { featureFlags } from "@rilldata/web-common/features/feature-flags.ts";
+ import { useFeatureFlags } from "@rilldata/web-common/features/feature-flags";
export let type: "home" | "modal";
export let open = false;
const runtimeClient = useRuntimeClient();
- const { developerChat } = featureFlags;
+ const { developerChat } = useFeatureFlags();
const FORM_ID = "generate-sample-data-form";
diff --git a/web-common/src/features/sources/modal/SourceImportedModal.svelte b/web-common/src/features/sources/modal/SourceImportedModal.svelte
index 757551aa669..08aacdcb6ac 100644
--- a/web-common/src/features/sources/modal/SourceImportedModal.svelte
+++ b/web-common/src/features/sources/modal/SourceImportedModal.svelte
@@ -13,13 +13,13 @@
import { MetricsEventSpace } from "../../../metrics/service/MetricsTypes";
import type { V1Resource } from "../../../runtime-client";
import { extractFileName } from "../../entity-management/file-path-utils";
- import { featureFlags } from "../../feature-flags";
+ import { useFeatureFlags } from "../../feature-flags";
import {
createCanvasDashboardFromTableWithAgent,
useCreateMetricsViewWithCanvasAndExploreUIAction,
} from "../../metrics-views/ai-generation/generateMetricsView";
- const { ai, developerChat } = featureFlags;
+ const { ai, developerChat } = useFeatureFlags();
export let sourcePath: string | null;
@@ -46,6 +46,7 @@
sourceName,
BehaviourEventMedium.Button,
MetricsEventSpace.Modal,
+ $ai,
)
: null;
@@ -73,6 +74,7 @@
"",
"",
sourceName,
+ $ai,
);
} else {
await createDashboardFromTable();
diff --git a/web-common/src/features/sources/navigation/SourceMenuItems.svelte b/web-common/src/features/sources/navigation/SourceMenuItems.svelte
index 9568a718e7e..9e58463d12d 100644
--- a/web-common/src/features/sources/navigation/SourceMenuItems.svelte
+++ b/web-common/src/features/sources/navigation/SourceMenuItems.svelte
@@ -5,7 +5,7 @@
import Model from "@rilldata/web-common/components/icons/Model.svelte";
import RefreshIcon from "@rilldata/web-common/components/icons/RefreshIcon.svelte";
import { fileArtifacts } from "@rilldata/web-common/features/entity-management/file-artifacts";
- import { featureFlags } from "@rilldata/web-common/features/feature-flags";
+ import { useFeatureFlags } from "@rilldata/web-common/features/feature-flags";
import { navigateToFile } from "@rilldata/web-common/layout/navigation/editor-routing";
import { getScreenNameFromPage } from "@rilldata/web-common/features/file-explorer/telemetry";
import { openResourceGraphQuickView } from "@rilldata/web-common/features/resource-graph/quick-view/quick-view-store";
@@ -48,7 +48,7 @@
$: ({ instanceId } = runtimeClient);
- const { ai, developerChat } = featureFlags;
+ const { ai, developerChat } = useFeatureFlags();
$: sourceQuery = fileArtifact.getResource(queryClient);
let source: V1Source | undefined;
@@ -88,6 +88,7 @@
false,
BehaviourEventMedium.Menu,
MetricsEventSpace.LeftPanel,
+ $ai,
);
$: createExploreFromTable = useCreateMetricsViewFromTableUIAction(
@@ -100,6 +101,7 @@
true,
BehaviourEventMedium.Menu,
MetricsEventSpace.LeftPanel,
+ $ai,
);
$: createCanvasDashboardFromTable = useCreateMetricsViewWithCanvasUIAction(
@@ -111,6 +113,7 @@
tableName,
BehaviourEventMedium.Menu,
MetricsEventSpace.LeftPanel,
+ $ai,
);
const handleCreateModel = async () => {
@@ -179,6 +182,7 @@
database,
databaseSchema,
tableName,
+ $ai,
);
} else {
await createCanvasDashboardFromTable();
diff --git a/web-common/src/layout/ApplicationHeader.svelte b/web-common/src/layout/ApplicationHeader.svelte
index ab60858a1ca..acce7b835f4 100644
--- a/web-common/src/layout/ApplicationHeader.svelte
+++ b/web-common/src/layout/ApplicationHeader.svelte
@@ -15,7 +15,7 @@
} from "@rilldata/web-common/features/dashboards/selectors.js";
import DeployProjectCTA from "@rilldata/web-common/features/dashboards/workspace/DeployProjectCTA.svelte";
import ExplorePreviewCTAs from "@rilldata/web-common/features/explores/ExplorePreviewCTAs.svelte";
- import { featureFlags } from "@rilldata/web-common/features/feature-flags.ts";
+ import { useFeatureFlags } from "@rilldata/web-common/features/feature-flags";
import { useProjectTitle } from "@rilldata/web-common/features/project/selectors";
import Header from "@rilldata/web-common/layout/header/Header.svelte";
import HeaderLogo from "@rilldata/web-common/layout/header/HeaderLogo.svelte";
@@ -27,7 +27,7 @@
import Tag from "../components/tag/Tag.svelte";
import { fileArtifacts } from "../features/entity-management/file-artifacts";
- const { deploy, developerChat, stickyDashboardState } = featureFlags;
+ const { deploy, developerChat, stickyDashboardState } = useFeatureFlags();
const runtimeClient = useRuntimeClient();
export let mode: string;
diff --git a/web-common/src/runtime-client/v2/RuntimeProvider.svelte b/web-common/src/runtime-client/v2/RuntimeProvider.svelte
index 1b9d1006f9f..3a69732caf9 100644
--- a/web-common/src/runtime-client/v2/RuntimeProvider.svelte
+++ b/web-common/src/runtime-client/v2/RuntimeProvider.svelte
@@ -1,7 +1,6 @@