Skip to content

feat(feature-flags): feature flags as primary authority with remoteComponents flag - #5559

Merged
Pavel910 merged 42 commits into
nextfrom
pavel/feat/feature-flags
Aug 5, 2026
Merged

feat(feature-flags): feature flags as primary authority with remoteComponents flag#5559
Pavel910 merged 42 commits into
nextfrom
pavel/feat/feature-flags

Conversation

@Pavel910

@Pavel910 Pavel910 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • FeatureFlags replaces WCP as the primary feature availability system. License is the gate; config (webiny.config.tsx) is the switch within the gate. Admin loads a featureFlags GraphQL query instead of wcp.getProject.
  • Single isEnabled("name") API with dot-path support (isEnabled("aiPowerups.cms.entryGeneration")). KnownFeatureFlag union provides autocomplete. No per-flag methods or decorator overrides needed.
  • remoteComponents feature flag added — gates the entire Remote Components extension (API + Admin) via <FeatureFlag.CanUseRemoteComponents>.
  • aiPowerups simplified to boolean | options union (no more enabled/options envelope).

Remote Components

Remote Components is a new enterprise feature that allows developers to create, edit, and bundle React components directly from the Webiny Admin UI, then load and render them in a Next.js frontend at runtime.

Key capabilities:

  • Admin UI — Monaco editor with live sandbox preview, AI-powered component generation and refinement
  • API — GraphQL CRUD, esbuild-wasm bundling pipeline with CSS scoping, SHA-256 integrity verification
  • Next.js runtimeGraphQLRemoteComponentLoader fetches and evaluates bundled components via the Webiny SDK

The remoteComponents feature flag gates the entire extension — when disabled, no GraphQL schema is registered and no admin UI (menu, routes, editor) is mounted.

Feature flags decision flow

1. No license              → false (everything off)
2. License blocks flag     → false (config ignored)
3. License allows + config=false → false
4. License allows + config unset → true (license is authority)
5. Not license-governed + license exists → config decides

Key changes

  • New featureFlags GraphQL query returns fully resolved flags
  • useFeatureFlags() replaces useWcp() in admin (~35 files migrated)
  • FeatureFlag.CanUse* components replace Wcp.CanUse* at config level
  • FeatureFlagsGate + MobX store ensures correct render timing
  • FeatureFlags export from webiny.config.tsx renders before extensions
  • Deleted Wcp.CanUse* components and WcpProvider from admin
  • API-side WcpContext.canUse*() consumers migrated to FeatureFlags
  • LICENSE_CHECKS map — single override point for license decoration
  • Repo skill: skills/repo-skills/add-feature-flag/SKILL.md

Test plan

  • Deploy with license + no config overrides → all licensed features enabled
  • Set recordLocking: false in config → disabled despite license allowing it
  • Set remoteComponents: false → Components menu and API schema absent
  • Remove FeatureFlags export from webiny.config.tsx → no error, license decides
  • yarn check passes (149 packages, only pre-existing @webiny/icons failure)

🤖 Generated with Claude Code

Pavel910 and others added 30 commits August 4, 2026 17:58
… next

Re-applied remote-components and sdk-nextjs changes on top of the
latest next branch. Resolved renames (handler-graphql -> api-graphql,
project-template-base extraction) and fixed lint/dependency issues.
… components

- Add remoteComponents?: boolean to IFeatureFlagsDto and isRemoteComponentsEnabled() to FeatureFlags class
- Create FeatureFlagsContext with module-level store, FeatureFlagsProvider, and useProjectFeatureFlags hook
- Create LicenseDecoratedFeatureFlags extending FeatureFlags with WCP license intersection
- Create FeatureFlag.CanUse* components in @webiny/project (CanUseRemoteComponents, CanUseMultiTenancy, etc.)
- Wire FeatureFlagsProvider into renderConfigWorker.tsx
- Update FeatureFlags extension to populate FeatureFlagsContext during render
- Gate RemoteComponents with FeatureFlag.CanUseRemoteComponents
- Restructure webiny.config.base.tsx: WebinyConfigTsx renders before RemoteComponents
- Migrate config-level Wcp.CanUse* to FeatureFlag.CanUse* (TenantManager, Languages, AiPowerups, webiny.config.tsx)
- Deprecate config-level Wcp.CanUse* components in @webiny/project
- Wire remoteComponents through WCP layer (ILicense, License, NullLicense, WcpContext, decorator, GraphQL, ReactLicense)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lags migration

Phase 2: API feature flags manifest
- Create FeatureFlagsSchemaFactory exposing featureFlags GraphQL query
- Create FeatureFlagsWithLicenseDecorator on API FeatureFlags abstraction
- Register decorator in WcpFeature, schema factory in ApiCoreFeature

Phase 3: Admin loads feature flags
- Create FeatureFlagsGateway, FeatureFlagsService, FeatureFlagsFeature
- Create FeatureFlagsProvider and useFeatureFlags hook
- Register in createRootContainer, add provider in Admin.tsx
- Migrate 13 consumer files from useWcp() to useFeatureFlags()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…icenseDecorator

Match build-level behavior: without a license, license-governed flags
are forced to false (NullLicense.canUse*() returns false).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lags

- app-workflows: Wcp.CanUseWorkflows → useFeatureFlags().isWorkflowsEnabled()
- app-website-builder-workflows: same
- app-headless-cms-workflows: same
- app-file-manager: Wcp.CanUsePrivateFiles → extracted PrivateFilesBulkEditField
- app-admin AccessManagementExtension: Wcp.CanUseTeams → extracted TeamsRoute/TeamsMenuItem
- app-website-builder LanguageCodeTag: Wcp.CanUseMultiTenancy → useFeatureFlags()

Zero Wcp.CanUse* usages remain outside backward-compat definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete packages/app-admin/src/components/Wcp.tsx (no consumers)
- Delete packages/project/src/components/Wcp.tsx (no consumers)
- Remove Wcp export from app-admin components/index.ts
- Remove Wcp export from project index.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rom admin

- Remove WcpProvider from Admin.tsx (no longer wraps the app)
- Remove WcpFeature registration from createRootContainer
- Comment out useWcp() in WebinyVersion.tsx (2 files) and Menus.tsx
- Remove useWcp export from app-admin index
- Remove useWcp re-export from app-serverless-cms

The admin app no longer fires the GetWcpProject GraphQL query.
Feature availability is now driven entirely by the featureFlags query.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…icenseDecorator

Use WcpLicenseProvider directly instead of WcpContext to avoid the cycle:
FeatureFlags → FeatureFlagsWithLicenseDecorator → WcpContext →
WcpContextWithFeatureFlagsDecorator → FeatureFlags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…reFlags

Migrate 14 files from WcpContext.canUse*() to FeatureFlags.get().is*Enabled():
- api-record-locking, api-workflows, api-website-builder-workflows
- api-audit-logs, api-file-manager, api-file-manager-s3
- api-core (IdentityContext, GetPermissionsFromIdentity)
- api-aco (FolderLevelPermissions)
- ai-powerups (AiImageEnrichment, BaseGraphQLSchema)
- app-aco (GetFolderLevelPermissionWithFlpDecorator)

Fix structuredClone failure on MobX proxies in FeatureFlags.toDto()
by using JSON.parse(JSON.stringify()) instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add toResolvedDto() to FeatureFlags that returns a complete IFeatureFlagsDto
with all flags explicitly set (using is*Enabled() methods), not just the
user overrides. The featureFlags GraphQL query now returns the full picture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All flags in toResolvedDto() are now flat boolean keys — no nested
objects for aiPowerups, fileManager, or advancedAccessControlLayer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ptions envelope

Structure preserved (aiPowerups, fileManager, advancedAccessControlLayer
are nested objects) but the enabled/options wrapper is removed — children
are direct boolean values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…olvedDto

When aiPowerups or advancedAccessControlLayer is disabled, return false
instead of an object with all children set to false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… aiPowerups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… at config level

Add CanUseAiPowerups to FeatureFlag components. Wrap AiPowerups extension
with it in AiPowerups.tsx so the entire extension (API + Admin) doesn't
mount when aiPowerups is disabled. Individual sub-feature checks happen
inside the extension when the parent flag passes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ons)

Change aiPowerups from { enabled, options } envelope to boolean | IAiPowerupsOptions,
matching the advancedAccessControlLayer pattern. Now supports:
- aiPowerups: false (disable entirely)
- aiPowerups: { websiteBuilder: { pageGeneration: false } } (sub-feature control)
- omit (enabled by default)

Remove unused IAiPowerupsFeatureFlags interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e config render

The renderConfigWorker now checks for webiny.features.ts at the project
root and loads it before rendering the config tree. This ensures feature
flags are set before any FeatureFlag.CanUse* component reads them,
regardless of render order.

The file exports a typed IFeatureFlagsDto object with full autocomplete:
  export default { aiPowerups: false } satisfies IFeatureFlagsDto;

Move feature flags from webiny.config.tsx <Project.FeatureFlags> to
webiny.features.ts. Fix AiPowerups to use CanUseAiPowerups (not
CanUseMultiTenancy).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The feature flags file exports a React component that renders
<Project.FeatureFlags>. The render worker mounts it before the main
config tree so setProjectFeatureFlags() fires during render, before
any FeatureFlag.CanUse* reads the flags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nstead of separate file

The template imports { FeatureFlags } from webiny.config.tsx and renders
it before DefaultExtensions. If FeatureFlags is not exported, null is
rendered. No separate webiny.features.tsx file needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ags are set

FeatureFlagsGate renders children only after setProjectFeatureFlags()
has been called. In webiny.config.base.tsx, <FeatureFlags /> renders
first (setting the flags), then FeatureFlagsGate opens and extensions
mount with the correct flag values.

If no FeatureFlags component is exported, skip=true bypasses the gate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace module-level variable + notification callback with a MobX
observable store. FeatureFlagsProvider and FeatureFlagsGate are MobX
observers — they re-render automatically when setProjectFeatureFlags()
updates the store.

Remove debug logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…decorator

- toDto() now returns the fully resolved state (all flags explicitly set)
- Remove toResolvedDto() — toDto() is the single method
- API FeatureFlagsWithLicenseDecorator uses class extension pattern
  (overrides is*Enabled methods) instead of DTO mutation + fromDto()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… isEnabled(name)

Single method: featureFlags.isEnabled('recordLocking') replaces
isRecordLockingEnabled(). Dot-path for nested flags:
isEnabled('aiPowerups.cms.entryGeneration').

KnownFeatureFlag union type provides autocomplete for known flag names.
FeatureFlagName = KnownFeatureFlag | (string & {}) allows custom flags.

Decorators override one method (isEnabled) with a LICENSE_CHECKS map.
Adding a new flag: update IFeatureFlagsDto + KnownFeatureFlag + Zod.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… link with feature flags

Delete both WebinyVersion.tsx files (unused).
Restore Upgrade menu link in Menus.tsx using featureFlags.isEnabled('multiTenancy')
instead of wcp.getProject() — show Upgrade when multiTenancy is disabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bulk action extension depends on CmsGenerateEntryContentUseCase
from ai-powerups. When aiPowerups is disabled, the use case is not
registered, causing a DI resolution error on /cms/manage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…witch

- Base isEnabled() returns false for undefined flags (disabled by default)
- License decorator: no license → all false; license blocks → false;
  license allows → config decides; not license-governed → needs license
  to exist, then config decides
- Config can disable what license allows, but cannot enable what
  license blocks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use namespace import to check if FeatureFlags exists in webiny.config.tsx
instead of named import that throws when the export is missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pavel910 and others added 5 commits August 5, 2026 14:40
Same pattern as AWS template: optional FeatureFlags import,
FeatureFlagsGate wrapping all extensions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add isExplicitlyDisabled() to distinguish 'not set' from 'set to false'.
When license allows a flag, it's enabled unless config explicitly disables it.
Config undefined = license decides. Config false = disabled even if licensed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Not license-governed yet — WCP backend doesn't know about it.
Without the entry, it falls through to 'license exists + config decides'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Pavel910 Pavel910 added this to the 6.6.0 milestone Aug 5, 2026
@Pavel910 Pavel910 self-assigned this Aug 5, 2026
# Conflicts:
#	.github/workflows/wac/utils/runNodeScripts/slopCop.js
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚓 Slop Cop

✅ Nothing worth flagging. The diff looks consistent with the PR's stated intent and the code-style rules.

Automated, non-blocking heads-up from an LLM. It can be wrong — use your judgment. Regenerates on every push.

Pavel910 and others added 6 commits August 5, 2026 15:43
IdentityContext now depends on FeatureFlags after migration from
WcpContext.canUse*(). Add BuildParamsFeature + FeatureFlagsFeature
to the test container setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sService

Replace WcpService mock with FeatureFlagsService mock after migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The bundler produces IIFE format (consumed via new Function() eval),
but the test used RemoteComponentLoader which does import() (expects ESM).
Replace with eval pattern matching how GraphQLRemoteComponentLoader and
the admin sandbox actually consume bundles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Pavel910
Pavel910 merged commit c6424ef into next Aug 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant