mirror of
https://github.com/supabase/supabase.git
synced 2026-06-18 05:33:50 +08:00
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>
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import enabledFeaturesRaw from './enabled-features.json' with { type: 'json' }
|
|
|
|
const knownFeatureKeys = Object.keys(enabledFeaturesRaw).filter((key) => key !== '$schema')
|
|
|
|
const ENV_PREFIX = 'ENABLED_FEATURES_'
|
|
|
|
// Server-only env var that short-circuits feature resolution; handled by
|
|
// isFeatureEnabled directly, not by this parser.
|
|
const RESERVED_ENV_NAMES = new Set<string>(['ENABLED_FEATURES_OVERRIDE_DISABLE_ALL'])
|
|
|
|
function featureKeyToEnvName(feature: string): string {
|
|
return ENV_PREFIX + feature.toUpperCase().replace(/[^A-Z0-9]/g, '_')
|
|
}
|
|
|
|
function parseBooleanEnv(raw: string): boolean | null {
|
|
const normalized = raw.trim().toLowerCase()
|
|
if (normalized === 'true') return true
|
|
if (normalized === 'false') return false
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Returns the list of feature keys disabled by ENABLED_FEATURES_* env vars.
|
|
* Server-only — these are not NEXT_PUBLIC_* and must be read at request time.
|
|
* Invalid values and prefixed env vars that don't match a known feature are
|
|
* logged and ignored.
|
|
*/
|
|
export function getEnabledFeaturesOverrideDisabledList(
|
|
env: Record<string, string | undefined>
|
|
): string[] {
|
|
const expected = new Map<string, string>()
|
|
for (const key of knownFeatureKeys) {
|
|
expected.set(featureKeyToEnvName(key), key)
|
|
}
|
|
|
|
const disabled: string[] = []
|
|
for (const [envName, featureKey] of expected) {
|
|
const raw = env[envName]
|
|
if (raw === undefined || raw === '') continue
|
|
const parsed = parseBooleanEnv(raw)
|
|
if (parsed === null) {
|
|
console.warn(
|
|
`[enabled-features] ${envName} must be "true" or "false" (got "${raw}"); ignoring.`
|
|
)
|
|
continue
|
|
}
|
|
if (parsed === false) disabled.push(featureKey)
|
|
}
|
|
|
|
for (const envName of Object.keys(env)) {
|
|
if (!envName.startsWith(ENV_PREFIX)) continue
|
|
if (expected.has(envName)) continue
|
|
if (RESERVED_ENV_NAMES.has(envName)) continue
|
|
console.warn(`[enabled-features] ${envName} does not match any known feature; ignoring.`)
|
|
}
|
|
|
|
return disabled
|
|
}
|