The dashboard runs against several deployment flavors (full cloud, OSS self-host, enterprise/on-prem). A feature flag lets each flavor turn a capability on or off without forking the code. Flags are the mechanism that keeps one frontend build serving every flavor.
All flags are defined in one place: frontend/src/lib/features.ts. Consume them with:
import { features } from "@/lib/features";
if (features.platform) {
// cloud-platform-only UI
}- Source of truth at runtime is the
VITE_FEATURE_FLAGSenv var: a comma-separated list of enabled flag names. - On cloud, PostHog feature flags are merged in additively on top of that. See PostHog flags.
- In dev (
import.meta.env.DEV), alocalStoragekeyFEATURE_FLAGSoverrides the env var so a flavor can be simulated locally. - Unset (
undefined) means every flag is on — that is the full cloud build. An empty/explicit list opts in only to the named flags. - Some flags imply others (e.g.
platformrequiresauth;aclis implied byplatform). Encode those dependencies infeatures.ts, not at each call site.
| Flag | Meaning |
|---|---|
auth |
Dashboard ships a login/register flow. NOT a proxy for "engine requires credentials." |
platform |
Cloud platform stack: publishable-token endpoint, billing, projects, multi-tenancy. Implies auth. (multitenancy is a legacy alias accepted during rollover.) |
acl |
Engine enforces token auth on the public endpoint. Implied by platform; set independently for enterprise. |
billing |
Billing UI. |
captcha |
Turnstile captcha on auth forms. Requires auth. |
compute |
Rivet Compute (managed pool) UI: namespace deployments + logs sidebar links and routes, actor-details deployment-logs tab, and the Rivet provider option in onboarding / Add Provider. Requires platform. |
support |
Support/help affordances. |
branding |
Rivet branding chrome. |
datacenter |
Datacenter-related UI. |
danger-zone |
Destructive settings actions (features.dangerZone). |
byoc |
Bring Your Own Cloud (features.byoc): the BYOC option in the create-project flow, BYOC clusters in the project lists, and the /orgs/$org/clusters/$cluster page. Requires platform. |
pallet |
v3 dashboard ("pallet") backed by cloud API v2: the single-page create cluster screen that replaces the create-project dialog and the /orgs/$org/new page, and pallet clusters on /orgs/$org/clusters/$cluster (checked before BYOC). Requires platform. |
services |
Managed services (features.services): the Services section of the onboarding product picker, the Durable Streams onboarding path, and the Services tab on the namespace settings drawer. Independent of platform (cloud hands out a managed service URL, self-host runs the worker container). |
mcp |
MCP connection settings card on the namespace settings drawer. Flavor-dependent content: platform renders the hosted mcp.rivet.dev endpoint pinned to this namespace, everything else renders the local stdio (npx @rivet-dev/mcp) config. |
Deployment flavors map to flag sets roughly as: cloud = all on; OSS = auth/platform/acl off; enterprise = acl on, auth/platform off (engine enforces auth without a login UI). Do not treat platform/auth as "engine requires credentials" — that is acl. compute is opt-in even on cloud — each Railway service adds it to VITE_FEATURE_FLAGS per-environment (e.g. staging on, prod off) rather than inheriting the cloud default-on set.
On deployments where PostHog is configured, PostHog feature flags are merged into features additively: a PostHog flag whose key matches a flag name (byoc, agent-os, danger-zone, ...) can turn that flag on, and can never turn off a flag that VITE_FEATURE_FLAGS or the dev localStorage override already enabled. Env and localStorage always win.
The merge is gated on PostHog actually being configured (getPosthogConfig in frontend/src/components/lib/config.ts), not on a flavor check, so OSS self-host and enterprise builds never load posthog-js and never wait on it.
The flag set is snapshotted once, in frontend/src/main.tsx, before the app module graph is imported. Several modules read features at import time and others use features.platform to guard conditional hook calls, so the flag set must be constant for the lifetime of the app. Later onFeatureFlags emissions are ignored, and the snapshot is capped by a short timeout so a slow or blocked PostHog cannot delay first paint. PostHog persists flags in localStorage, so a flag flipped in the PostHog UI applies from the next page load.
To test, set the flag on your own PostHog person and reload, or simulate it locally before the app boots:
// In DevTools, on a cloud-flavored dashboard
localStorage.setItem("FEATURE_FLAGS", ""); // env baseline: everything off
// then enable e.g. `byoc` for your user in PostHog and reloadA bug can exist in only one flavor. Whatever flavor your dev server happens to default to hides breakage in the others, so testing a single flavor is not enough. OSS is the most important and the most commonly regressed flavor because it turns the most off (auth/platform/acl all off), so any code that assumes cloud context (org/project params, auth session, cloud data providers) silently breaks there. The OSS namespace dropdown, sidebar, context switcher, and onboarding all take different code paths than cloud.
Before considering any frontend change done, verify it in at least OSS and cloud. If a surface renders conditionally on features.* (or differs by flavor at all: sidebar, context switcher, onboarding, settings, auth, billing), exercise each affected flavor in the browser, not just the default.
Switch flavors in dev without restarting the server by setting the localStorage override and reloading (the dev build reads localStorage.FEATURE_FLAGS ahead of VITE_FEATURE_FLAGS):
// OSS self-host: everything off
localStorage.setItem("FEATURE_FLAGS", ""); location.reload();
// Full cloud: all flags on (see the commented canonical list in frontend/.env.local)
localStorage.setItem(
"FEATURE_FLAGS",
"compute,platform,acl,auth,captcha,branding,support,billing,datacenter,danger-zone,multitenancy,mcp,byoc,services",
);
location.reload();
// Enterprise: acl on, no login UI
localStorage.setItem("FEATURE_FLAGS", "acl,branding,support,datacenter,danger-zone"); location.reload();In an agent-browser / DevTools session, paste those into the page console. localStorage persists across reloads, so run localStorage.removeItem("FEATURE_FLAGS") to return to the env default when finished. Confirm the active flavor with JSON.stringify(features) after importing, or just observe whether auth/cloud chrome is present.
A new pack of features must ship behind a feature flag whenever it is significant or not universally available across deployment flavors. The goal is that every flavor can freely enable or disable it.
Add a flag when the feature:
- Is unavailable, restricted, or behaves differently on at least one deployment flavor (cloud / OSS / enterprise), OR
- Introduces a substantial new surface (a whole panel, page, settings section, or subsystem) that a flavor may want to turn off.
Do not add a flag for:
- Small, universal changes (bug fixes, copy tweaks, layout polish, a single button that every flavor always shows).
- Anything that is always on everywhere — that is just code.
Avoid flag sprawl: do not gate everything. One flag per meaningful capability, not per component.
If you are unsure whether a feature needs a flag, confirm with the user before adding (or omitting) one. When in doubt, ask rather than guessing — flags are hard to remove once flavors depend on them.
- Add the flag in
features.tswith a one-line comment on what it gates and any implied dependencies; do not readVITE_FEATURE_FLAGSorimport.meta.envdirectly elsewhere. - Name flags after the capability (
billing,support), not the deployment (enterprise-only). Deployment flavor is composed from flags, not the other way around. - Encode flag-implies-flag relationships in
features.tsso call sites stay simple booleans. - When a flag changes the set of flavors, update the deployment-flavor mapping above and any docs that describe what each flavor ships.