Webiny-js webiny-v5-to-v6-migration
git clone https://github.com/webiny/webiny-js
T=$(mktemp -d) && git clone --depth=1 https://github.com/webiny/webiny-js "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/user-skills/api/v5-to-v6-migration" ~/.claude/skills/webiny-webiny-js-webiny-v5-to-v6-migration && rm -rf "$T"
skills/user-skills/api/v5-to-v6-migration/SKILL.mdv5 → v6 Migration Patterns
Overview
v6 replaces v5's plugin-based architecture with feature-based DI. The key shifts:
| v5 Concept | v6 Equivalent |
|---|---|
| + (Service) |
| Plugin array | + |
| DI injection via constructor |
| feature |
| |
Pattern 1: Context Plugin → DI Service
v5
new ContextPlugin(async context => { context.lingotekService = { translate: async (docId, locale) => { /* ... */ }, getStatus: async docId => { /* ... */ }, deleteProject: async projectId => { /* ... */ } }; });
v6
// features/lingotekService/abstractions.ts import { createAbstraction } from "@webiny/feature/api"; export interface ILingotekService { translate(docId: string, locale: string): Promise<Result<void, Error>>; getStatus(docId: string): Promise<Result<TranslationStatus, Error>>; deleteProject(projectId: string): Promise<Result<void, Error>>; } export const LingotekService = createAbstraction<ILingotekService>("MyExt/LingotekService"); export namespace LingotekService { export type Interface = ILingotekService; } // features/lingotekService/LingotekService.ts class LingotekServiceImpl implements LingotekService.Interface { constructor(private buildParams: BuildParams.Interface) {} async translate(docId: string, locale: string) { /* ... */ } async getStatus(docId: string) { /* ... */ } async deleteProject(projectId: string) { /* ... */ } } export default LingotekService.createImplementation({ implementation: LingotekServiceImpl, dependencies: [BuildParams] }); // features/lingotekService/feature.ts export const LingotekServiceFeature = createFeature({ name: "LingotekService", register(container) { container.register(LingotekServiceImpl).inSingletonScope(); } });
Key difference: v5 attaches to context object. v6 uses DI — consumers declare the service as a constructor dependency.
Pattern 2: Event Subscription → EventHandler Feature
v5
context.cms.onEntryAfterCreate.subscribe(async params => { if (params.model.modelId !== "myModel") return; await doSomething(params.entry); });
v6
// features/syncOnCreate/EntryAfterCreateHandler.ts import { EntryAfterCreateEventHandler } from "webiny/api/cms/entry"; import { LingotekService } from "../lingotekService/abstractions.js"; import { MY_MODEL_ID } from "~/shared/constants.js"; class SyncOnCreateHandler implements EntryAfterCreateEventHandler.Interface { constructor(private lingotekService: LingotekService.Interface) {} async handle(event: EntryAfterCreateEventHandler.Event) { const { entry, model } = event.payload; if (model.modelId !== MY_MODEL_ID) return; await this.lingotekService.translate(entry.entryId, "en"); } } export default EntryAfterCreateEventHandler.createImplementation({ implementation: SyncOnCreateHandler, dependencies: [LingotekService] }); // features/syncOnCreate/feature.ts export const SyncOnCreateFeature = createFeature({ name: "SyncOnCreate", register(container) { container.register(SyncOnCreateHandler); } });
Key differences:
- Feature directory is named by business capability (
), not by event namesyncOnCreate - Handler is a thin orchestrator — business logic lives in the injected service
- Must filter by
— handler fires for ALL modelsmodel.modelId
Pattern 3: Plugin Array → Feature Registration
v5
export default () => [ new GraphQLSchemaPlugin({ ... }), new ContextPlugin(async ctx => { ... }), myModelPlugin, eventSubscriptionPlugin ];
v6
// api/Extension.ts import { createFeature } from "webiny/api"; export const Extension = createFeature({ name: "MyExtension", register(container) { container.register(MyModel); container.register(MyGraphQLSchema); SyncOnCreateFeature.register(container); LingotekServiceFeature.register(container); } });
Pattern 4: Async Service Bootstrap → ServiceProvider Pattern
When a v5 service was initialized with async data (loading settings, fetching config), v6 uses the ServiceProvider pattern — a provider abstraction with
async getService() that lazily creates and caches the service.
See the ServiceProvider Pattern section in webiny-api-architect for the full pattern with abstractions, implementation, and consumer examples.
Pattern 6: Permissions objects
v5
[ { name: "content.i18n", locales: ["en-US"] }, { name: "cms.endpoint.read" }, { name: "cms.endpoint.manage" }, { name: "cms.endpoint.preview" }, { name: "cms.contentModelGroup", groups: { "en-US": [LT_TRANSLATION_MODEL_GROUP_ID] }, rwd: "rw", own: false, pw: "" }, { name: "cms.contentModel", models: { "en-US": [ LT_TRANSLATION_DOCUMENT_MODEL_ID, LT_CONFIG_MODEL_ID, LT_TRANSLATION_PROJECT_MODEL_ID ] }, rwd: "rwd", own: false, pw: "" }, { name: "cms.contentEntry", rwd: "rwd", own: false, pw: "" } ];
v6
no longer existscontent.i18n- locale codes no longer exist
is an array ofmodels
stringsmodel.modelId
is an array ofgroups
stringsgroup.slug
[ { name: "cms.endpoint.read" }, { name: "cms.endpoint.manage" }, { name: "cms.endpoint.preview" }, { name: "cms.contentModelGroup", groups: ["LT_TRANSLATION_MODEL_GROUP_ID"], rwd: "rw", own: false, pw: "" }, { name: "cms.contentModel", models: [ "LT_TRANSLATION_DOCUMENT_MODEL_ID", "LT_CONFIG_MODEL_ID", "LT_TRANSLATION_PROJECT_MODEL_ID" ], rwd: "rwd", own: false, pw: "" }, { name: "cms.contentEntry", rwd: "rwd", own: false, pw: "" } ];
Type Resolution Guide
When working with Webiny abstractions, always verify types from source before writing code.
Step 1: Find the catalog entry
Use MCP skills or generated catalogs to look up the abstraction (e.g.,
RoleFactory).
Step 2: Get the source path
The catalog entry includes a
Source field pointing to the abstraction definition.
Step 3: Read the type definition
# Read the abstractions file cat node_modules/@webiny/api-core/features/security/roles/shared/abstractions.d.ts
Common type patterns
| Pattern | What to expect |
|---|---|
| Factories | Return or |
| UseCases | Have type and return |
| EventHandlers | Have with property |
| Repositories | Return — wrap CMS errors |
Migration Map: v5 → v6 Equivalents
Backend: Context Method Calls → Use Cases
| v5 Pattern | v6 Equivalent |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| DI container registration |
| DI container injection |
Backend: Lifecycle Event Subscriptions → EventHandlers
v5 Pattern () | v6 EventHandler |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
Backend: Plugin Classes → v6 Equivalents
| v5 Plugin | v6 Equivalent |
|---|---|
| DI-registered implementations |
| DI-registered implementations |
| |
| |
| |
| |
| TODO |
| |
| |
| TODO |
| TODO (Adrian) |
| TODO |
| TODO |
| TODO |
Admin: React Plugins → AdminConfig API
| v5 Pattern | v6 Equivalent |
|---|---|
| |
| |
/ menu components | |
| or with new schema |
| TODO |
| |
| |
| / |
| |
Common Migration Mistakes
1. Creating one abstraction per operation
v5 habit: separate plugins per action. v6: group related operations into a multi-method Service.
2. Naming features by technical event
v5 habit: thinking in terms of hooks (
onEntryAfterCreate). v6: features describe business capability (syncToLingotek). Files inside can be named technically (EntryAfterCreateHandler.ts).
3. Assuming builder patterns
v6 factories sometimes return plain objects, sometimes builder objects. Always read source types first, to understand what the factory in question returns.
4. Putting event handlers in a handlers/ directory
v5 habit: grouping by type. v6: handlers are features — they go in
features/.
5. Attaching to context
v5:
context.myService = { ... }. v6: create an abstraction and register it in the DI container via the parent feature, or a standalone feature (createFeature).
6. Inline business logic in event handlers
v5 habit: putting logic directly in the subscription callback. v6: handlers are thin orchestrators — extract logic into a Service or UseCase.
Related Skills
- webiny-api-architect — Full v6 architecture, Services vs UseCases, anti-patterns
- webiny-use-case-pattern — UseCase implementation details
- webiny-event-handler-pattern — EventHandler and domain event patterns
- webiny-dependency-injection — DI pattern and injectable services