From e5a201fac6cce0852c336eb7d42e4b7ea1305f26 Mon Sep 17 00:00:00 2001 From: Mateus Saggin Date: Wed, 19 Aug 2026 14:32:39 -0300 Subject: [PATCH] docs(vtex-io-app-settings): cover major-version scoping of saved settings Saved app settings are addressed by the app's major version range, so they persist across minor and patch releases but start empty on a new major. This was missing from the skill, and it fails quietly: the app just reads schema defaults, so a flag that gated a behavior reverts and the feature silently turns off for every merchant with no error to detect. Verified against the Apps API, where only the major locator resolves: vtex.@3.x returns the saved settings while @3.6.1 and @3.0.0 both return app_not_found. Adds decision rules, a hard constraint with paired examples, failure modes and checklist items covering fail-safe defaults, re-applying settings during a major upgrade, and keeping kill switches in version-independent storage. --- exports/agents-md/vtex-io/AGENTS.md | 53 +++++++++++++++++++ .../claude/vtex-io-vtex-io-app-settings.md | 53 +++++++++++++++++++ exports/claude/vtex-io.md | 53 +++++++++++++++++++ exports/copilot/copilot-instructions.md | 53 +++++++++++++++++++ exports/copilot/vtex-io.md | 53 +++++++++++++++++++ exports/cursor/vtex-io-all.mdc | 53 +++++++++++++++++++ .../cursor/vtex-io-vtex-io-app-settings.mdc | 53 +++++++++++++++++++ exports/kiro/steering/vtex-io-all.md | 53 +++++++++++++++++++ .../steering/vtex-io-vtex-io-app-settings.md | 53 +++++++++++++++++++ .../opencode/vtex-io-app-settings/SKILL.md | 53 +++++++++++++++++++ rules/vtex-io-all.mdc | 53 +++++++++++++++++++ rules/vtex-io-vtex-io-app-settings.mdc | 53 +++++++++++++++++++ skills/vtex-io-app-settings/SKILL.md | 53 +++++++++++++++++++ .../skills/vtex-io-app-settings/skill.md | 53 +++++++++++++++++++ 14 files changed, 742 insertions(+) diff --git a/exports/agents-md/vtex-io/AGENTS.md b/exports/agents-md/vtex-io/AGENTS.md index 28c5a77..456cdcf 100644 --- a/exports/agents-md/vtex-io/AGENTS.md +++ b/exports/agents-md/vtex-io/AGENTS.md @@ -622,6 +622,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -791,6 +795,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -837,6 +885,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -847,6 +898,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/claude/vtex-io-vtex-io-app-settings.md b/exports/claude/vtex-io-vtex-io-app-settings.md index be5643f..a8e2bb5 100644 --- a/exports/claude/vtex-io-vtex-io-app-settings.md +++ b/exports/claude/vtex-io-vtex-io-app-settings.md @@ -36,6 +36,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -205,6 +209,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -251,6 +299,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -261,6 +312,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/claude/vtex-io.md b/exports/claude/vtex-io.md index a3df1a9..b609494 100644 --- a/exports/claude/vtex-io.md +++ b/exports/claude/vtex-io.md @@ -611,6 +611,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -780,6 +784,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -826,6 +874,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -836,6 +887,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/copilot/copilot-instructions.md b/exports/copilot/copilot-instructions.md index 7ea525b..0ec8f21 100644 --- a/exports/copilot/copilot-instructions.md +++ b/exports/copilot/copilot-instructions.md @@ -10316,6 +10316,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -10485,6 +10489,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -10531,6 +10579,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -10541,6 +10592,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/copilot/vtex-io.md b/exports/copilot/vtex-io.md index 6dc781b..04afb25 100644 --- a/exports/copilot/vtex-io.md +++ b/exports/copilot/vtex-io.md @@ -607,6 +607,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -776,6 +780,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -822,6 +870,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -832,6 +883,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/cursor/vtex-io-all.mdc b/exports/cursor/vtex-io-all.mdc index 2b3f2b5..f211ef1 100644 --- a/exports/cursor/vtex-io-all.mdc +++ b/exports/cursor/vtex-io-all.mdc @@ -611,6 +611,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -780,6 +784,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -826,6 +874,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -836,6 +887,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/cursor/vtex-io-vtex-io-app-settings.mdc b/exports/cursor/vtex-io-vtex-io-app-settings.mdc index 23f37c2..d0d15c8 100644 --- a/exports/cursor/vtex-io-vtex-io-app-settings.mdc +++ b/exports/cursor/vtex-io-vtex-io-app-settings.mdc @@ -40,6 +40,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -209,6 +213,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -255,6 +303,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -265,6 +316,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/kiro/steering/vtex-io-all.md b/exports/kiro/steering/vtex-io-all.md index ab5199e..337ad4d 100644 --- a/exports/kiro/steering/vtex-io-all.md +++ b/exports/kiro/steering/vtex-io-all.md @@ -607,6 +607,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -776,6 +780,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -822,6 +870,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -832,6 +883,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/kiro/steering/vtex-io-vtex-io-app-settings.md b/exports/kiro/steering/vtex-io-vtex-io-app-settings.md index 61a9ad8..b06bfe3 100644 --- a/exports/kiro/steering/vtex-io-vtex-io-app-settings.md +++ b/exports/kiro/steering/vtex-io-vtex-io-app-settings.md @@ -38,6 +38,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -207,6 +211,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -253,6 +301,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -263,6 +314,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/exports/opencode/vtex-io-app-settings/SKILL.md b/exports/opencode/vtex-io-app-settings/SKILL.md index 988c20d..7e5874b 100644 --- a/exports/opencode/vtex-io-app-settings/SKILL.md +++ b/exports/opencode/vtex-io-app-settings/SKILL.md @@ -39,6 +39,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -208,6 +212,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -254,6 +302,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -264,6 +315,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/rules/vtex-io-all.mdc b/rules/vtex-io-all.mdc index 2b3f2b5..f211ef1 100644 --- a/rules/vtex-io-all.mdc +++ b/rules/vtex-io-all.mdc @@ -611,6 +611,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -780,6 +784,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -826,6 +874,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -836,6 +887,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/rules/vtex-io-vtex-io-app-settings.mdc b/rules/vtex-io-vtex-io-app-settings.mdc index 23f37c2..d0d15c8 100644 --- a/rules/vtex-io-vtex-io-app-settings.mdc +++ b/rules/vtex-io-vtex-io-app-settings.mdc @@ -40,6 +40,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -209,6 +213,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -255,6 +303,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -265,6 +316,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/skills/vtex-io-app-settings/SKILL.md b/skills/vtex-io-app-settings/SKILL.md index 988c20d..7e5874b 100644 --- a/skills/vtex-io-app-settings/SKILL.md +++ b/skills/vtex-io-app-settings/SKILL.md @@ -39,6 +39,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -208,6 +212,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -254,6 +302,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -264,6 +315,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference diff --git a/tracks/vtex-io/skills/vtex-io-app-settings/skill.md b/tracks/vtex-io/skills/vtex-io-app-settings/skill.md index 99febc9..5b61927 100644 --- a/tracks/vtex-io/skills/vtex-io-app-settings/skill.md +++ b/tracks/vtex-io/skills/vtex-io-app-settings/skill.md @@ -67,6 +67,10 @@ Do not use this skill for: - Pixel apps that need configuration should also consume settings through `ctx.clients.apps.getAppSettings(...)` on the backend side of the pixel app. If a value must be available to injected JavaScript, expose only non-sensitive fields through `access: "public"` and `publicSettingsForApp`, keeping secrets strictly on the server side. - Make code resilient to missing or incomplete settings by validating or applying defaults at the consumption boundary. - Never assume settings are identical across accounts or workspaces. Each workspace may have different app configuration during development, rollout, or debugging. +- Saved settings are scoped to the app's **major** version range, not to the full version. They persist across minor and patch releases, and they do **not** carry over to a new major. +- Because settings are keyed by major, choose defaults that are safe when the stored value is absent. After a major bump every merchant reads schema defaults until settings are re-applied, and a feature flag defaulting to `false` silently turns the feature off with no error. +- For values that must survive a major bump, such as operational kill switches, do not rely on `settingsSchema` alone. Persist them under a version-independent key (for example VBase keyed by the app name with the `@version` suffix stripped) and accept that you then own the admin surface for editing them. +- Plan a settings re-apply step into any major-version upgrade runbook, and verify the settings of the new major before considering the rollout complete. Settings vs configuration builder: @@ -236,6 +240,50 @@ If a proposed setting stores records that behave like orders, reviews, logs, or } ``` +### Constraint: Settings must not be assumed to survive a major version bump + +Saved app settings are scoped to the app's major version range. Configuration that must outlive a major bump MUST either be re-applied as part of the upgrade or stored under a version-independent key. + +**Why this matters** + +Settings are addressed by major range, so `vtex.my-app@3.x` and `vtex.my-app@4.x` are separate configuration scopes. Releasing `3.6.1` after `3.6.0` keeps every saved value, but releasing `4.0.0` starts from an empty settings object. + +This fails quietly. Nothing errors: the app simply reads schema defaults, so a flag that gated a behavior reverts to its default and the feature silently turns off for every merchant. Reading with the full version at runtime hides the distinction, because `getAppSettings(process.env.VTEX_APP_ID)` resolves `vtex.my-app@3.6.1` to the `3.x` scope for you. + +**Detection** + +If a major version bump is planned and any behavior depends on a saved setting, STOP and decide explicitly whether each value is re-applied after the upgrade or moved to version-independent storage. If a setting acts as a kill switch or protects against an incident, treat `settingsSchema` alone as insufficient. + +**Correct** + +```typescript +// Fail-safe default: losing the stored value degrades to the previous +// behavior rather than changing it. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) +const useNewPricing = settings.useNewPricing ?? false + +// For a switch that must survive a major bump, key it without the version +// so the same key is read before and after the upgrade. +const [appName] = String(process.env.VTEX_APP_ID).split('@') +const killSwitch = await ctx.clients.vbase.getJSON('app_runtime_flags', appName, true) +``` + +**Wrong** + +```typescript +// Assumes the merchant's saved value is still there after 4.0.0, and +// silently disables the integration when it is not. +const settings = await ctx.clients.apps.getAppSettings( + ctx.vtex.appId ?? process.env.VTEX_APP_ID +) + +if (settings.integrationEnabled) { + await runIntegration() +} +``` + ### Constraint: Code must validate or default settings at the consumption boundary Settings-dependent code MUST tolerate missing or incomplete values safely. @@ -282,6 +330,9 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - Adding workspace-level policies such as `read-workspace-apps` or invalid policies such as `write-workspace-apps` as a generic workaround for app settings permission errors, instead of validating the correct appId and standard app-settings permissions. - Using `settingsSchema` when the requirement is really block-level Store Framework configuration. - Creating schemas that are too broad or vague. +- Assuming saved settings carry over to a new major version, so a feature silently reverts to its default after the upgrade. +- Choosing a default that changes behavior when the stored value is missing, instead of a fail-safe one. +- Keeping an operational kill switch only in `settingsSchema`, where a major bump resets it. ## Review checklist @@ -292,6 +343,8 @@ Use frontend GraphQL access only for intentionally public settings, and keep bac - [ ] Are secrets kept backend-only and never exposed to the frontend? - [ ] If `access: "public"` is used, are all exposed settings intentionally safe for frontend consumption? - [ ] Is the settings surface small and intentional? +- [ ] Is each default fail-safe, so that losing the stored value degrades rather than changes behavior? +- [ ] If a major version bump is planned, is there a step to re-apply settings, and do any kill switches live in version-independent storage instead? ## Reference