Supabase enabled-features 特性开关体系:从静态 JSON 到 Studio 自托管运行时覆盖的完整指南
【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase
本指南围绕仓库中 enabled-features 包文档,系统讲解 Supabase 跨 Studio、docs、www 三大前端统一使用的静态特性开关机制:单一事实来源enabled-features.json的维护方式、isFeatureEnabled调用 API 的两种形态、自托管 Studio 通过ENABLED_FEATURES_*环境变量在容器启动时覆盖开关的规则,以及新增一个开关的完整步骤。读完你既能在业务代码中正确消费特性开关,也能为自托管部署精准裁剪 UI 功能。
定位:一套 JSON 驱动的跨应用特性开关源
在 Supabase 主仓库中,apps/下同时存在 Studio(管理控制台)、docs(文档站)、www(官网)等多个前端应用,它们共享大量功能页面与导航入口。为了避免各应用各自硬编码“某功能是否出现”,仓库把特性开关收敛到一个独立包中统一管理。
该包的目录结构与职责如下(packages/common/enabled-features):
| 文件 | 职责 |
|---|---|
enabled-features.json | 特性开关的单一事实来源(source of truth),构建期读取 |
enabled-features.schema.json | 每个开关 key 的 JSON Schema 描述与required校验 |
index.ts | 导出isFeatureEnabled()、类型Feature及 camelCase 转换实现 |
overrides.ts | 解析ENABLED_FEATURES_*环境变量,输出被运行时禁用的开关列表 |
overrides.test.ts | 对上述解析逻辑的 vitest 单测 |
README.md | 包级使用文档(本文的主体依据) |
包通过 packages/common/index.tsx 以export * from './enabled-features'对外再导出,业务侧既可用common的聚合入口,也可按文档示例直接import ... from 'common/enabled-features'。
从架构上看,这套体系是“编译期静态默认值 + 可选运行时禁用列表”的组合:所有开关在 JSON 中以true/false给出构建期默认值;运行时(仅 Studio 自托管场景)可额外叠加“禁用列表”进一步收紧,但不会反转静态默认值。
单一事实来源:enabled-features.json
所有开关都定义在 enabled-features.json 中,文件的第二行"$schema": "./enabled-features.schema.json"声明了配套的 JSON Schema,IDE 可以据此获得补全与校验。
文件中每个 key 采用域:功能(如logs:templates)或域_域:功能(如dashboard_auth:sign_up)的命名风格,按功能域分组。以当前仓库中的真实开关为例:
{ "logs:all": true, "logs:templates": true, "logs:collections": true, "logs:metadata": true, "logs:show_metadata_ip_template": true, "docs:self-hosting": true, "docs:full_platform": true, "branding:large_logo": false, "search:fullIndex": true, "project_settings:custom_domains": true, "project_settings:restart_project": true }这些开关覆盖的功能面非常广,从 enabled-features.schema.json 的description字段可以归纳出几大类用途:
- 页面/导航显隐:如
logs:all描述为“Enable the logs pages and navigation. Disable to hide all logs pages”,即当自托管环境未配置 Logflare 等日志后端时,可一次性隐藏全部日志页面与导航入口; - 局部 UI 元素开关:如
profile:show_email控制工具栏是否展示用户邮箱,project_connection:show_orms控制连接弹窗中的 ORMs 标签页; - SDK/文档内容裁剪:如
sdk:python、sdk:swift、docs:auth_flows等,用于决定 docs 站渲染哪些文档章节; - 登录方式控制:如
dashboard_auth:sign_in_with_github、dashboard_auth:sign_up; - 灰度中的新功能:默认值刻意关闭,如
branding:large_logo(false)。
从当前 JSON 实况看,绝大多数开关默认为
true,仅少数新功能默认关闭。关闭项会被 index.ts 收集进“静态禁用集合”,这是后面解析逻辑的起点。
消费开关:isFeatureEnabled 的两种调用形态
使用方通过isFeatureEnabled判断开关,文档给出的基础用法如下:
import { isFeatureEnabled } from 'common/enabled-features' if (isFeatureEnabled('logs:templates')) { // ... } const { logsTemplates, logsMetadata } = isFeatureEnabled(['logs:templates', 'logs:metadata'])关键设计点:传入单个 key 返回boolean;传入数组则返回一个 camelCase 化的对象,方便解构消费。例如传入'logs:templates',返回对象上的键是logsTemplates。
其类型签名在 index.ts 中定义:
export type Feature = Profile['disabled_features'][number] | keyof typeof enabledFeaturesStaticObj function isFeatureEnabled<T extends Feature[]>( features: T, runtimeDisabledFeatures?: Feature[] ): { [key in FeatureToCamelCase<T[number]>]: boolean } function isFeatureEnabled(features: Feature, runtimeDisabledFeatures?: Feature[]): boolean有两个值得注意的类型级细节:
Feature类型由两部分并集构成——Profile['disabled_features'][number](即平台账号资料中可被远程下发的禁用项,类型来自api-types包的ProfileResponseschema)与enabled-features.json中的全部静态 key。因此仓库内已知的开关 key 与账号资料可下发的禁用 key 都被类型系统覆盖,传入不存在的 key 会报编译错误。FeatureToCamelCase<S>是模板字符串类型,在类型层面把a_b:c_d计算为aBCd这样的 camelCase 形态,让数组重载返回的对象键具备精确推导。
对应的运行时转换featureToCamelCase实现为:先把:全部替换成_,再按_切分,首词保持小写、其余词首字母大写后拼接:
function featureToCamelCase(feature: Feature) { return feature .replace(/:/g, '_') .split('_') .map((word, index) => (index === 0 ? word : word[0].toUpperCase() + word.slice(1))) .join('') as FeatureToCamelCase<typeof feature> }例如logs:show_metadata_ip_template→logsShowMetadataIpTemplate,branding:large_logo→brandingLargeLogo。注意首段的 snake_case 也会被转换,因此返回对象键与原始 key 并不一一对应为“去冒号”,使用时以解构出的 camelCase 属性为准。
isFeatureEnabled的第二个可选参数runtimeDisabledFeatures?: Feature[]用于叠加运行时禁用项(例如来自登录账号资料),其判定逻辑是把“运行时禁用列表”与“JSON 中为false的静态禁用列表”合并成一个Set,只要 key 在集合中即为关闭:
const disabledFeatures = new Set([ ...(runtimeDisabledFeatures ?? []), ...disabledFeaturesStaticArray, ]) function checkFeature(feature: Feature, features: Set<Feature>) { return !features.has(feature) }数组形态则对每个 key 逐个计算并组装成 camelCase 对象。从中可以确认一个约束:运行时覆盖只能“关闭”静态为true的开关,无法“打开”静态为false的开关,因为机制本身只维护禁用集合。
Studio 侧:useIsFeatureEnabled 的分层聚合
文档特别指出:在 Studio 中应优先使用useIsFeatureEnabled(位于 apps/studio/hooks/misc/useIsFeatureEnabled.ts),而不是直接调用裸函数。原因在于该 Hook 在静态 JSON 之上,又叠加了两层来源:
const { profile } = useProfile() const { data: override } = useEnabledFeaturesOverrideQuery() const disabledFeatures = [ ...(profile?.disabled_features ?? []), ...((override?.disabled_features ?? []) as Feature[]), ]useProfile()读取认证账号资料(对应平台侧/platform/profile接口返回的disabled_features);useEnabledFeaturesOverrideQuery()(apps/studio/data/misc/enabled-features-override-query.ts)请求/api/enabled-features-overrides这个 Next.js 接口,拿到自托管运行时覆盖的结果。
两者拼接为禁用列表后再调用isFeatureEnabled。这意味着对 React 组件而言,开关的最终状态取决于“JSON 默认值 + 账号级禁用 + 部署级禁用”的叠加。
运行时覆盖:自托管 Studio 的环境变量机制
为什么需要运行时覆盖
自托管部署(self-hosted)的场景下,运维方不希望为了隐藏某个页面而重新构建镜像。因此该包支持在容器启动时通过环境变量按开关禁用功能,无需修改代码、无需重新 build。
环境变量命名规则
前缀为ENABLED_FEATURES_,每个开关对应一个环境变量。命名规则为:将开关 key 大写,并把所有非字母数字字符(:、_、-)替换为_:
ENABLED_FEATURES_LOGS_ALL=false ENABLED_FEATURES_LOGS_TEMPLATES=false ENABLED_FEATURES_BRANDING_LARGE_LOGO=true该映射规则的底层实现位于 overrides.ts:
function featureKeyToEnvName(feature: string): string { return ENV_PREFIX + feature.toUpperCase().replace(/[^A-Z0-9]/g, '_') }因此branding:large_logo与docs:self-hosting的转换如下:
| 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 |
值的解析与容错
环境变量取值必须为true或false(大小写不敏感,且会先trim()去除首尾空白),对应实现:
function parseBooleanEnv(raw: string): boolean | null { const normalized = raw.trim().toLowerCase() if (normalized === 'true') return true if (normalized === 'false') return false return null }解析逻辑的整体行为(overrides.ts 的getEnabledFeaturesOverrideDisabledList)遵循以下规则:
- 以
enabled-features.json中已知 key 为基准,预先构建“预期环境变量名 → 开关 key”的映射表; - 只处理映射表中出现的环境变量:值为
false的加入禁用列表;值为true的不做任何事; - 非法取值会被打印警告并忽略:
[enabled-features] ${envName} must be "true" or "false" (got "..."); ignoring.; - 前缀正确但不对应任何已知开关的环境变量同样被警告并忽略(用于拦截拼写错误),例如
ENABLED_FEATURES_NOT_A_FEATURE=false会触发 “does not match any known feature” 警告; ENABLED_FEATURES_OVERRIDE_DISABLE_ALL是保留名,不走上述匹配逻辑,不产生误报;- 空字符串按“未设置”处理,不警告。
这些边界行为都有对应单测覆盖,见 overrides.test.ts(如“accepts booleans case-insensitively and trims whitespace”“warns and ignores non-boolean values”“warns and ignores env vars prefixed but not matching a known feature”)。
为什么映射是“前向只读”的
文档强调该映射是forward-only(已知 key → 期望 env 名)。因为反向推导不唯一:若允许“env 名反推 key”,ENABLED_FEATURES_BRANDING_LARGE_LOGO既可以来自branding:large_logo,也可以来自branding_large:logo。而实现只从已知 key 单向计算期望的 env 名,再拿进程环境去匹配,天然规避了这种 snake_case 与冒号的碰撞歧义。
后端链路:API 路由如何把环境变量暴露给前端
自托管覆盖之所以能生效,关键在一条 Next.js API 路由。自托管 Studio 部署中,apps/studio/pages/api/enabled-features-overrides.ts 在请求时读取服务端环境变量:
if (IS_PLATFORM) { return res.status(200).json({ disabled_features: [] }) } return res.status(200).json({ disabled_features: getEnabledFeaturesOverrideDisabledList(process.env), })要点有两个:
IS_PLATFORM分流:托管平台(hosted)返回空禁用列表,路由形同 no-op;只有自托管(IS_PLATFORM === false)才真正解析环境变量。- 服务端专用:这些变量不是
NEXT_PUBLIC_*,前端构建产物里不包含,必须在请求时读取process.env。这正是 README 中“runtime override is surfaced through a Next.js API route and React Query”的含义——docs 与 www 不使用这套 React Query/API 路由体系,它们的开关解析停留在构建期读取enabled-features.json。
前端侧由useEnabledFeaturesOverrideQuery消费该接口,再经 useIsFeatureEnabled.ts 合并进禁用列表。
最终判定顺序
综合文档说明与源码实现,Studio 运行期某个开关是否开启的解析链如下:
ENABLED_FEATURES_*环境变量(仅自托管:IS_PLATFORM === true时该层为 no-op);其解析得到运行时禁用列表;profile.disabled_features(来自/platform/profile的认证账号资料);enabled-features.json的静态值(false项进入静态禁用集合);- 默认值(开关开启)。
需要澄清的一点:步骤 1–3 在实现上并不是“取第一个命中值”的覆盖关系,而是把各来源的禁用项合并为集合后统一判定;任何一层声明“禁用”都会关闭该开关,且没有任何一层能反向启用一个被上层关闭或静态为false的开关。
作用域与已知限制
Studio 专属
运行时覆盖仅对 Studio 生效。docs 与 www 不依赖 Next.js API 路由与 React Query,其开关在构建期即固化,无法在容器启动时通过这套环境变量覆盖。
托管平台不受影响
托管平台侧 API 路由返回空禁用列表,托管 Studio 的功能裁剪只来源于认证账号的disabled_features,与自托管环境变量完全解耦。
“闪现未样式”窗口
在useEnabledFeaturesOverrideQuery的请求返回之前,组件会先用 JSON 静态值渲染。对于“JSON 中开启、但运行时覆盖要禁用”的开关,会出现短暂先展示后隐藏的窗口。这是 Hook 时序导致的固有表现,设计 UI 时需要知悉。
Support.constants.ts 属于构建期调用点
apps/studio/components/interfaces/Support/Support.constants.ts 在模块加载时直接调用isFeatureEnabled('billing:all'),用其结果构建CATEGORY_OPTIONS,随后展开进 Zod 表单 schema。由于它在模块顶层执行,走的是静态 JSON 路径,无法被运行时覆盖;若要让该开关也支持运行时覆盖,需要一次更大的重构(例如改为惰性求值或接入 Hook)。这是当前体系下一处明确记录在案的边界。
全量禁用开关:给工具链用的逃生舱
针对文档嵌入/搜索索引生成这类“需要一份过滤版页面清单”的工具场景,包提供了服务器专用的一键全禁用变量:
ENABLED_FEATURES_OVERRIDE_DISABLE_ALL=true该变量会短路所有开关:isFeatureEnabled在入口处先检查它,命中即把所有查询的开关判定为false(index.ts 的前置分支)。其注意事项:
- server-only:不带
NEXT_PUBLIC_前缀,只应在服务端进程设置; - 优先级高于其他一切来源(文档原话 “short-circuits over everything else”);
- 它被
overrides.ts显式列入RESERVED_ENV_NAMES,避免被“未知 env 名”警告误报,单测也专门覆盖了这一点。
其注释说明了用途:文档嵌入管线用它配合常规搜索索引的同步构建方式,产出经功能裁剪后的过滤索引。
新增一个开关的标准流程
当某个新功能需要接入这套开关体系时,按文档步骤操作即可,且必须同步修改两个文件以保持一致性:
- 在 enabled-features.json 中加入 key 并设定期望的默认值(发布中功能建议默认
false); - 在 enabled-features.schema.json 中补充该 key 的
type: "boolean"与一句人读description,并把它加入顶层required数组。
schema 的required数组与additionalProperties: false共同构成约束:JSON 里出现的每个开关都必须被 schema 描述,schema 要求的每个开关 JSON 里都必须存在,双方不一致即可被发现。加入后:
- 类型系统会自动将其纳入
Feature联合类型(keyof typeof enabledFeaturesStaticObj),错误的 key 会在编译期报错; overrides.ts的“已知 key → env 名”映射表由 JSON 动态生成,新开关的ENABLED_FEATURES_*变量即刻可用;- 各消费方通过
isFeatureEnabled或 Studio 的useIsFeatureEnabled接入即可。
总结:一套文件、两层来源、三种消费方式
回到整体设计,可以把它压缩为几个关键记忆点:
- 一套文件:所有开关以声明式 JSON 维护,schema 保证描述与校验不漂移;
- 两层来源:静态默认值(构建期)+ 禁用列表(账号资料 / 自托管环境变量,运行期),最终状态是两者的交集——任何一层禁用即禁用;
- 三种消费方式:通用场景裸调
isFeatureEnabled;Studio 组件用useIsFeatureEnabled自动聚合账号与运行时覆盖;docs/www 维持构建期静态解析。
对自托管运维而言,最常用的落地姿势就是在容器启动时写入若干ENABLED_FEATURES_*环境变量,在“不重建镜像”的前提下按部署环境裁剪 Studio 的功能面(例如关闭日志页、隐藏计费与支持入口等),同时利用ENABLED_FEATURES_OVERRIDE_DISABLE_ALL=true为内容索引工具产出全量过滤版本。
进一步阅读可在仓库中查看:包文档、开关全集 JSON、schema 定义、解析与类型实现、环境变量解析、解析单测、Studio 聚合 Hook、API 路由。
【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考