Lets self-hosted Studio toggle flags in `enabled-features.json` at container start time via `ENABLED_FEATURES_*` env vars, without rebuilding the prebuilt image. Addresses [FE-3036](https://linear.app/supabase/issue/FE-3036/allow-enabled-featuresjson-flags-to-be-overridden-via-env-vars) and is a prerequisite for [COM-205](https://linear.app/supabase/issue/COM-205/add-feature-flag-to-disable-all-logs-in-studio). **Added:** - `packages/common/enabled-features/overrides.ts` — pure parser that maps `ENABLED_FEATURES_*` env vars to a disabled-features list (forward-only key mapping, boolean validation, typo warnings) + 10 vitest tests - `apps/studio/pages/api/enabled-features-overrides.ts` — Next.js API route reading `process.env` at request time; no-op (`{ disabled_features: [] }`) when `IS_PLATFORM` - `apps/studio/data/misc/enabled-features-override-query.ts` — React Query hook with `staleTime: Infinity`, `enabled: !IS_PLATFORM` - `packages/common/enabled-features/README.md` — docs the env var convention, resolution order, `IS_PLATFORM` gating, and the `Support.constants.ts` build-time caveat **Changed:** - `apps/studio/hooks/misc/useIsFeatureEnabled.ts` — merges the override's `disabled_features` with `profile.disabled_features` ### Env var shape One var per flag, prefixed `ENABLED_FEATURES_`. Feature key → env name: uppercase with every non-alphanumeric char replaced by `_`. ```bash ENABLED_FEATURES_LOGS_ALL=false ENABLED_FEATURES_BRANDING_LARGE_LOGO=true ``` Values are `true`/`false` case-insensitively. Other values and prefixed vars that don't match a known feature are logged and ignored. ### Resolution order (runtime, Studio only) 1. `ENABLED_FEATURES_*` (self-hosted, via API route → React Query → hook) 2. `profile.disabled_features` (hosted, from `/platform/profile`) 3. `enabled-features.json` static value 4. Default (enabled) `ENABLED_FEATURES_OVERRIDE_DISABLE_ALL` still short-circuits everything. ### Known limitation `apps/studio/components/interfaces/Support/Support.constants.ts:4` calls `isFeatureEnabled('billing:all')` at module load to build `CATEGORY_OPTIONS`, which is spread into Zod form schemas. That call site stays resolved from the JSON — documented in the package README. `billing:all` isn't on the radar for self-hosted runtime toggling. ## To test - `cd packages/common && pnpm exec vitest run enabled-features` — 10 new tests pass - `pnpm --filter studio run typecheck` clean - Spin Studio locally with `NEXT_PUBLIC_IS_PLATFORM=false` and `ENABLED_FEATURES_LOGS_TEMPLATES=false`; `/project/[ref]/logs/explorer/templates` should reflect the flag after the override fetch resolves - Confirm the API route returns `{ disabled_features: [] }` when `NEXT_PUBLIC_IS_PLATFORM=true` - Set a typo like `ENABLED_FEATURES_LOGS_TMEPLATES=false` and check the warning in container logs; flag stays enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Runtime feature-flag overrides for self-hosted deployments (env var driven), new API endpoint and client-side hook to fetch overrides, and client logic now merges profile and runtime overrides. * **Documentation** * Added comprehensive README describing the feature-flag system and override configuration. * **Tests** * Added unit tests for override parsing and E2E tests covering runtime override behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
4.0 KiB
enabled-features
Static feature-flag source used across Studio, docs, and www. The source of truth is enabled-features.json; every flag listed there is enabled (true) or disabled (false) at build time.
Using a flag
import { isFeatureEnabled } from 'common/enabled-features'
if (isFeatureEnabled('logs:templates')) {
// ...
}
const { logsTemplates, logsMetadata } = isFeatureEnabled(['logs:templates', 'logs:metadata'])
Passing an array returns a camelCased object so consumers can destructure.
isFeatureEnabled(feature, runtimeDisabledFeatures?) accepts an optional list of features disabled at runtime (e.g. from the authenticated profile) that compose on top of the static defaults.
In Studio, prefer useIsFeatureEnabled from @/hooks/misc/useIsFeatureEnabled — it layers the authenticated profile's disabled_features and the self-hosted runtime override (below) on top of the static JSON.
Runtime override (Studio self-hosted)
Self-hosted Studio deployments can disable flags at container start time without rebuilding the image. Set one env var per flag under the ENABLED_FEATURES_ prefix:
ENABLED_FEATURES_LOGS_ALL=false
ENABLED_FEATURES_LOGS_TEMPLATES=false
ENABLED_FEATURES_BRANDING_LARGE_LOGO=true
Env var values are true / false (case-insensitive). Any other value is logged and ignored.
Key → env name mapping
Uppercase the feature key and replace every non-alphanumeric character (:, _, -) with _:
| Feature key | Env var name |
|---|---|
logs:all |
ENABLED_FEATURES_LOGS_ALL |
branding:large_logo |
ENABLED_FEATURES_BRANDING_LARGE_LOGO |
docs:self-hosting |
ENABLED_FEATURES_DOCS_SELF_HOSTING |
logs:show_metadata_ip_template |
ENABLED_FEATURES_LOGS_SHOW_METADATA_IP_TEMPLATE |
Mapping is forward-only (known key → expected env name), so snake_case vs colon collisions like branding:large_logo / branding_large:logo never come up.
Env vars prefixed with ENABLED_FEATURES_ that don't match a known feature are logged and ignored — a catch for typos.
Resolution order (Studio runtime)
ENABLED_FEATURES_*env vars (self-hosted only — route is a no-op whenIS_PLATFORM === true)profile.disabled_featuresfrom/platform/profileenabled-features.jsonstatic value- Default (enabled)
Scope & limitations
- Studio only. The runtime override is surfaced through a Next.js API route and React Query, which docs and www don't use — their flag resolution stays build-time from
enabled-features.json. - Hosted is unaffected. The API route returns an empty list when
IS_PLATFORM === true; hosted Studio continues to derive disabled features exclusively from the authenticated profile. - Brief flash of unstyled flags. Before the override fetch resolves, flags default to their JSON value. For features gated as "enabled in JSON, disabled by runtime override", there's a brief window where the UI shows the feature before it's hidden.
Support.constants.tscall site is build-time only.apps/studio/components/interfaces/Support/Support.constants.tscallsisFeatureEnabled('billing:all')at module load to buildCATEGORY_OPTIONS, which is then spread into Zod form schemas. That one call site resolves from the static JSON and cannot be runtime-overridden without a larger refactor.
Disable everything (for tooling)
ENABLED_FEATURES_OVERRIDE_DISABLE_ALL=true forces every flag to false. Used by the docs embeddings pipeline to produce a filtered index. This is a server-only env (no NEXT_PUBLIC_ prefix) and short-circuits over everything else.
Adding a new flag
- Add the key to
enabled-features.jsonwith the desired default. - Add the key and a description to
enabled-features.schema.json, including it in therequiredarray.