Migration guide - step-by-step instructions
We recommend following the migration steps in order. If you have questions, please contact us.
Avoid breaking changes
Migrating from SDK v1 to v2 should not cause breaking changes for your users. Please avoid including any breaking changes to your extension’s functionality while migrating. If your extension does include breaking changes, please contact us to discuss.
0. Prepare to migrate
Section titled “0. Prepare to migrate”We recommend that you create a temporary project on Netlify to deploy your migrated extension to. You can use this project to install and test the extension in production without impacting your current users.
- Create a new Git branch for your extension and push the branch to your repository.
- Navigate to your team in the Netlify UI and select Add a new project > Import an existing project.
- Follow the prompts to configure a new project based on the branch. Make sure you update the value for Branch to deploy and select the branch from step 1.
- Deploy the project.
As you work on the migration, you can commit and push updates to your Git repository, and then deploy the project to reflect your changes. Once your migrated extension is ready, you can follow the steps documented below to replace the published integration with the code from this branch.
1. Update dependencies
Section titled “1. Update dependencies”First, start by updating the required dependencies for your project.
-
Install
@netlify/sdk@latestand@netlify/netlify-plugin-netlify-extension@latest -
Update the
netlify.tomlfile in your project to include@netlify/netlify-plugin-netlify-extensionnetlify.toml [[plugins]]package = "@netlify/netlify-plugin-netlify-extension"
2. Update files in the project root folder
Section titled “2. Update files in the project root folder”Next, update the package.json, netlify.toml, and integration.yaml files in the project root folder.
Update package.json and netlify.toml
Section titled “Update package.json and netlify.toml”In SDK v2, the SDK commands have been renamed from netlify-integration to netlify-extension, and we’ve made the following changes:
netlify-extension builddoes not accept a--watchargument. You should usenetlify-extension devas a replacement.netlify-integration previewhas been removed. You should usenetlify-extension devas a replacement.
Update the package.json in your extension project to update the SDK commands:
"scripts": { "build": "netlify-integration build -a", "dev": "netlify-integration dev -c", "preview": "netlify-integration preview" "build": "netlify-extension build", "dev": "netlify-extension dev --open", },Along with the command changes, there are some other important setting changes to make in the extension’s netlify.toml file:
[build]command="netlify-integration build -a"command="netlify-extension build -a"
publish=".ntli/site/static"
[functions]directory=".ntli/site/netlify/functions"directory = "src/endpoints"node_bundler = "esbuild"
[[headers]]for="/ui/*"[headers.values]Access-Control-Allow-Origin="*"Access-Control-Allow-Headers="*"Access-Control-Allow-Methods="*"
[dev]autoLaunch = falsefunctions = ".ntli/site/netlify/functions"command = "netlify-extension dev"
[[plugins]]package = "@netlify/netlify-plugin-netlify-extension"The functions directory changes now that API Handlers are endpoints, and you no longer need to set the node_bundler and headers with the new extension UI.
Rename and update the integration.yaml file
Section titled “Rename and update the integration.yaml file”There are two changes to make to the configuration file:
- Rename the
integration.yamlfile toextension.yaml. If the file ends in.yml, rename toextension.yml. - Remove
integrationLevelfrom theextension.yamlfile. All extensions are installed on the team level automatically, so this can’t be customized anymore.
3. Edit methods that have been replaced or updated
Section titled “3. Edit methods that have been replaced or updated”We’ve replaced and updated a number of methods and APIs. The following sections outline how to update your extension to reflect these changes. We have another section below for changes specific to connectors.
- Import
NetlifyExtensionand export{ extension } - Replace addApiHandler with endpoint files
- Replace
context.providerOAuthTokenwithcontext.auth - Replace
onEnableandonDisablewith endpoints - Replace
getSiteIntegrationwithgetSiteConfiguration - Replace
updateSiteIntegrationwithupdateSiteConfiguration - Replace
enableTeamIntegrationwithcreateTeamConfiguration - Replace
enableSiteIntegrationwithcreateSiteConfiguration - Replace
getTeamIntegrationwithgetTeamConfiguration - Replace
updateTeamIntegrationwithupdateTeamConfiguration
Import NetlifyExtension and export { extension }
Section titled “Import NetlifyExtension and export { extension }”In your src/index.ts file, update the code to import and call NetlifyExtension, and to export a valid extension object.
import { NetlifyIntegration } from "@netlify/sdk"; import { NetlifyExtension } from "@netlify/sdk";
const integration = new NetlifyIntegration(); const extension = new NetlifyExtension();
// ... any other logic
export { integration }; export { extension };Replace addApiHandler with endpoint files
Section titled “Replace addApiHandler with endpoint files”API Handlers are now called endpoints in SDK v2. By default, they are now stored in individual files inside the src/endpoints directory, and use the latest Netlify Functions syntax instead of Lambda-compatible syntax.
integration.addApiHandler('custom-name', async (event, context) => { // ...logic return { body: JSON.stringify({}), statusCode: 200 };});import { withNetlifySDKContext } from "@netlify/sdk/ui/functions";
export default withNetlifySDKContext(async (req, context) => { // ...logic return Response.json({});});Note that you will need to update your endpoints from the Lambda-compatible event and response objects to the web-standard Request and Response objects.
If you haven’t already, make sure to update the extension’s netlify.toml file to reflect the directory change. You’ll also need to update any paths for calling your endpoints, in your code or while testing locally, from /.netlify/functions/handler/your-endpoint-name to /.netlify/functions/your-endpoint-name.
Replace context.providerOAuthToken with context.auth
Section titled “Replace context.providerOAuthToken with context.auth”If your extension uses OAuth and accesses the token in an API Handler, replace context.providerOAuthToken with context.auth.
First make the above mentioned updates to replace addApiHandler and then update the file to use context.auth.
import { withNetlifySDKContext } from "@netlify/sdk/ui/functions";
export default withNetlifySDKContext((req, context) => { const { providerOAuthToken } = context; const { providerToken } = context.auth; // use the token to make authenticated requests to your API
return Response.json({ message: "Hello World" });});Replace onEnable and onDisable with endpoints
Section titled “Replace onEnable and onDisable with endpoints”Remove the onEnable and onDisable methods and move your custom logic for the install and uninstall flows into new endpoints that your extension UI will call. More on how to convert your integration UI to extension UI and call endpoints is documented below.
Note that extensions are installed on the team level only. If your integration customized logic for project-level flows, you can specify this using createSiteConfiguration and deleteSiteConfiguration.
integration.onEnable(() => { // ... logic to run after the integration is enabled for a team }
integration.onDisable(() => { // ... logic to run before the integration is disabled for a team }import { withNetlifySDKContext } from "@netlify/sdk/ui/functions";
export default withNetlifySDKContext(async (req, context) => { /* ...logic to run after the extension is installed on a team or configured on a project */ return Response.json({});});import { withNetlifySDKContext } from "@netlify/sdk/ui/functions";
export default withNetlifySDKContext(async (req, context) => { /* ...logic to run before the extension is uninstalled from a team or configuration is removed from a project */ return Response.json({});});Replace getSiteIntegration with getSiteConfiguration
Section titled “Replace getSiteIntegration with getSiteConfiguration”If you’re using getSiteIntegration in your extension, replace it with getSiteConfiguration. You will need to pass the teamId in addition to the siteId and add a check for configurationResponse.
Before:
export default withNetlifySDKContext(async (req, { client, siteId }) => { if (siteId) { const config = await client.getSiteIntegration(siteId); return Response.json(config); } return Response.json({}, { status: 500 });});After:
export default withNetlifySDKContext(async (req, { client, siteId, teamId }) => { if (siteId && teamId) { const configurationResponse = await client.getSiteConfiguration( teamId, siteId );
if (configurationResponse) { const { config } = configurationResponse; return Response.json(config); } } return Response.json({}, { status: 500 });});Replace updateSiteIntegration with updateSiteConfiguration
Section titled “Replace updateSiteIntegration with updateSiteConfiguration”If you’re using updateSiteIntegration in your extension, replace it with updateSiteConfiguration. You will need to pass the teamId in addition to the siteId.
Before:
export default withNetlifySDKContext(async (req, { client, siteId }) => { if (siteId && body) { const { config: updatedConfig } = JSON.parse(body); const { config } = await client.getSiteIntegration(siteId);
const newConfig = { ...config, ...updatedConfig, };
await client.updateSiteIntegration(siteId, newConfig);
return Response.json({}); } return Response.json({}, { status: 500 });});After:
export default withNetlifySDKContext(async (req, { client, siteId, teamId }) => { if (siteId && body && teamId) { const { config: updatedConfig } = JSON.parse(body); const configurationResponse = await client.getSiteConfiguration( teamId, siteId );
if (configurationResponse) { const { config } = configurationResponse; const newConfig = { ...(config as {}), ...updatedConfig, };
await client.updateSiteConfiguration(teamId, siteId, newConfig);
return Response.json({}); } } return Response.json({}, { status: 500 });});Replace enableTeamIntegration with createTeamConfiguration
Section titled “Replace enableTeamIntegration with createTeamConfiguration”If you’re using enableTeamIntegration in your extension to create a team configuration, replace it with createTeamConfiguration.
export default withNetlifySDKContext(async (req, { client, siteId, teamId }) => { if (teamId && body) { const { config } = JSON.parse(body); await client.enableTeamIntegration(teamId, config); await client.createTeamConfiguration(teamId, config);
return Response.json({}); } return Response.json({}, { status: 500 }); });Replace enableSiteIntegration with createSiteConfiguration
Section titled “Replace enableSiteIntegration with createSiteConfiguration”If you’re using enableSiteIntegration in your extension, replace it with createSiteConfiguration and pass in the teamId.
export default withNetlifySDKContext(async (req, { client, siteId, teamId }) => { const body = await req.json(); if (teamId && body) { const { config } = JSON.parse(body); await client.enableSiteIntegration(siteId, config); await client.createSiteConfiguration(teamId, siteId, config);
return Response.json({}); } return Response.json({}, { status: 500 }); });Replace getTeamIntegration with getTeamConfiguration
Section titled “Replace getTeamIntegration with getTeamConfiguration”If you’re using getTeamIntegration in your extension, replace it with getTeamConfiguration and add a check for configurationResponse.
export default withNetlifySDKContext(async (req, { client, teamId }) => { if (teamId) { const config = await client.getTeamIntegration(teamId); const configurationResponse = await client.getTeamConfiguration(teamId);
if (configurationResponse) { const { config } = configurationResponse; return Response.json(config); } } return Response.json({}, { status: 500 });});Replace updateTeamIntegration with updateTeamConfiguration
Section titled “Replace updateTeamIntegration with updateTeamConfiguration”If you’re using updateTeamIntegration in your extension, replace it with updateTeamConfiguration, pass in the siteId, and check for configurationResponse.
Your code likely also uses getTeamIntegration before making the call to update, so you will also need to replace that with getTeamConfiguration as noted in the above section.
export default withNetlifySDKContext(async (req, { client, siteId, teamId }) => { const body = await req.json(); if (teamId && body) { const { config: updatedConfig } = JSON.parse(body); const { config } = await client.getTeamIntegration(teamId); const configurationResponse = await client.getTeamConfiguration(teamId, siteId);
if (configurationResponse) { const { config } = configurationResponse; const newConfig = { ...config, ...(config as {}), ...updatedConfig, }; await client.updateTeamIntegration(teamId, newConfig); await client.updateTeamConfiguration(teamId, newConfig); return Response.json({}); } } return Response.json({}, { status: 500 }););4. Remove methods and fields that no longer exist
Section titled “4. Remove methods and fields that no longer exist”The following methods and fields are at end of service and should be removed from your extension:
- Remove
enableBuildEventHandlersanddisableBuildEventHandlers. Build event handlers run on every project when a user installs an extension on a team. If this is undesirable behavior, refer to the section on the migration overview page to learn how you can add a safeguard for this.
5. Replace Integration UI with extension UI
Section titled “5. Replace Integration UI with extension UI”We are excited about the brand new extension UI functionality and the opportunity it provides to build rich user experiences on top of the React ecosystem. However, because it is a complete replacement, migrating Integration UI to extension UI is not a straightforward change.
To help with this migration, the SDK includes a migration helper that will add extension UI boilerplate files to your new extension.
The following sections provide some more details about the change, how to migrate, and some sample code.
What’s changed
Section titled “What’s changed”Integration UI is a custom UI framework that sends messages to the Netlify UI, communicating what UI state and interactivity the Netlify UI should set up on your extension’s behalf. Extension UI, on the other hand, allows you to write a single-page React application that Netlify renders to sandboxed iFrames in different locations within the Netlify UI.
The way extensions work is also more flexible than before. While extensions were previously installed to projects, they are now installed once on a team and then configured on individual projects. To support this flexibility, your UI and endpoints may need to change to accommodate this change. For example, while Integration UI surfaces always modified a project, some extension UI surfaces exist to modify a team instead.
While migrating your UI involves manual work, we hope you’ll ultimately find building on top of the React ecosystem easier and more familiar than working with a bespoke UI framework.
For more information about all of the components and surfaces available, refer to the extension UI reference docs.
How to migrate
Section titled “How to migrate”The specifics might differ depending on what you built previously but here are some rough steps for migrating to extension UI:
- Read through the extension UI documentation to get a sense of how it works.
- Run
npm create @netlify/sdk@latest -- --uiMigrationto add extension UI boilerplate files to your extension. - Modify the generated extension details and top-level project section surfaces for your extension.
- The extension details page is a new surface. If your extension offers an OAuth connection, you should add a
<ProviderAuthConnection />component to your extension details page surface. - If your extension has a connector, and you answered yes to this question during the
--uiMigrationwizard, you should also have surfaces generated for Connect.
- The extension details page is a new surface. If your extension offers an OAuth connection, you should add a
- If required, move any endpoints defined using
addApiHandler()in yoursrc/index.tsinto individual files in thesrc/endpointsdirectory. For example, an API handler defined byaddApiHandler("my-function", ...)should be moved tosrc/endpoints/my-function.ts.- You’ll also need to update your functions from Lambda-compatible syntax to use the latest Netlify Functions syntax and wrap them with
withNetlifySDKContextto access the Netlify SDK client. Refer to the endpoints documentation for an example. - If you haven’t already, make sure to update the extension’s
netlify.tomlfile to reflect the directory change. - You’ll also need to update any paths for calling your endpoints, in your code or while testing locally, from
/.netlify/functions/handler/your-endpoint-nameto/.netlify/functions/your-endpoint-name.
- You’ll also need to update your functions from Lambda-compatible syntax to use the latest Netlify Functions syntax and wrap them with
- If your endpoints include any project-specific logic, make sure they are not invoked by surfaces that operate only on teams. For example, the extension details page surface.
- Finally, when you’re ready, remove your old Integration UI files including
src/ui/index.ts.
Migrated form example
Section titled “Migrated form example”Because migrating from Integration UI to extension UI involves a change of frameworks, and because each extension has a different UI experience, it’s challenging to give detailed instructions for how to migrate from one solution to the other.
To provide a sense of how the code might change, here is an example of a form created with Integration UI and a new version created with extension UI.
With Integration UI, a form within a card might be defined like this:
const route = new SurfaceRoute("/");
route.addSection( { id: "configure-section", title: "Configure your Contentful space", }, (section) => { section.addForm( { id: "configure-form", title: "Connected space", onSubmit: async ({ surfaceInputsData }) => { // Do something with collected form data }, }, (form) => { form.addInputText({ id: "username", label: "Password", });
form.addInputPassword({ id: "password", label: "Password", });
form.addInputSelect({ id: "animal", label: "Animal", options: [ { value: "dog", label: "Dog" }, { value: "cat", label: "Cat" }, ], }); } ); });The equivalent UI in extension UI would be:
import { Card, CardTitle, Form, FormField, FormFieldSecret, Select,} from "@netlify/sdk/ui/react/components";
export const SiteSettings = () => { const onSubmit = (values) => { // Do something with form data };
return ( <Card> <CardTitle>Configure Your Extension</CardTitle>
<Form onSubmit={onSubmit}> <FormField name="username" label="Username" /> <FormFieldSecret name="password" label="password" />
<Select name="animal" label="Animal" options={[ { label: "Dog", value: "dog" }, { label: "Cat", value: "cat" }, ]} /> </Form> <Card> );};6. Update code for connectors
Section titled “6. Update code for connectors”We’ve made a few changes to the SDK that you need to apply to your extension.
- Define which feature the connector supports
- Set
autoFormatGraphQLTypesAndFieldsto true - Update removed connector-related APIs and fields
- Add extension UI for your connector
Define which feature the connector supports
Section titled “Define which feature the connector supports”All connectors must specify which Netlify features it supports.
const connector = extension.addConnector({ typePrefix: 'Example', supports: { connect: true, // or false if not supported },});Set autoFormatGraphQLTypesAndFields to true
Section titled “Set autoFormatGraphQLTypesAndFields to true”All existing connectors should set autoFormatGraphQLTypesAndFields to true. Once set to true, Netlify will format GraphQL field and type names to use pascal and camel case. Previously, this would happen automatically for all connectors.
If autoFormatGraphQLTypesAndFields is set to false or not defined at all, GraphQL fields like field_name in the original data source will be field_name in the GraphQL API. Projects that currently query a data layer’s GraphQL API will be expecting fieldName instead.
To avoid breaking API queries for your users, set the autoFormatGraphQLTypesAndFields to true:
const connector = extension.addConnector({ typePrefix: 'Example', autoFormatGraphQLTypesAndFields: true, supports: { connect: true, },});Update removed connector-related APIs and fields
Section titled “Update removed connector-related APIs and fields”The following connector-specific API and fields are no longer available.
- Remove
enableConnectorsanddisableConnectors - Replace
define.nodeModelwithdefine.document - Replace
connector.eventwithconnector.sync - Replace
connector.defineOptionswith an update toaddConnector - Replace
connector.initwith an update toaddConnector
Remove enableConnectors and disableConnectors
Section titled “Remove enableConnectors and disableConnectors”Both of these methods have been removed. When a user enables an extension on a team, we automatically enable the connector for you.
Replace define.nodeModel with define.document
Section titled “Replace define.nodeModel with define.document”If your connector uses define.nodeModel, that API has been replaced by define.document.
define.nodeModel({define.document({ name: "Post", cacheFieldName: "updatedAt", fields: { title: { type: "String", }, updatedAt: { type: "String", required: true, }, },});Replace connector.event with connector.sync
Section titled “Replace connector.event with connector.sync”If your connector uses connector.event, that API has been replaced by connector.sync.
If your connector uses this API, you can pass a single function to connector.sync() instead and use isInitialSync to determine which logic to run.
connector.event(`createAllNodes`, customSyncAllDataFn);connector.event(`updateNodes`, customSyncChangedDataFn);
connector.sync(({ isInitialSync, models }) => { if (isInitialSync) { return customSyncAllDataFn(models); } else { return customSyncChangedDataFn(models); }});If you previously set connector.event('updateNodes', false) to ensure every sync is a full data sync (with no caching), you need to configure your connector with deltaSync set to false. This will disable caching between data syncs:
extension.addConnector({ supports: { connect: true, deltaSync: false, },});Replace connector.defineOptions with an update to addConnector
Section titled “Replace connector.defineOptions with an update to addConnector”Update your extension to remove connector.defineOptions and instead define options in extension.addConnector:
As part of the migration, you will also need to add extension UI for your connector and you will be able to remove the .meta properties for each of these options then. For now, you can leave them in for reference while you build your extension UI.
connector.defineOptions(({ zod }) => { extension.addConnector({ defineOptions: ({ zod }) => { return zod.object({ apiToken: zod.string().meta({ label: "API token", helpText: "The delivery API token for your environment", secret: true, }), pageLimit: zod.string().optional().meta({ label: "Page limit", helpText: "The number of entries to fetch per page when syncing data", }), }); }});Replace connector.init with an update to addConnector
Section titled “Replace connector.init with an update to addConnector”Update your extension to remove connector.init and instead define the initial state in extension.addConnector:
connector.init(({ options }) => { extension.addConnector({ initState: ({ options }) => { const apiClient = new CustomCMSAPIClient({ url: options.apiURL, token: options.apiToken, });
return { apiClient // this will be available as state.apiClient in other connector APIs }; }});Add extension UI for your connector
Section titled “Add extension UI for your connector”With SDK v2, connectors must include code to add extension UI that we will render in the Netlify UI for Connect users — this is in addition to defining configuration options in the extension itself.
To add extension UI for your connector, follow these steps:
- Read through the extension UI documentation to get a sense of how it works.
- Run
npm create @netlify/sdk@latest -- --uiMigrationto add extension UI boilerplate files to your extension. When you receive the promptDoes your extension have a connector?, make sure you specify yes and the boilerplate will include a surface for Connect. - Update the boilerplate files to create a configuration form to display for users in Connect when they add a data source using your extension. The fields should match what you defined as configuration options. Refer to the add a surface doc for an example of a form you can add.
- Add an endpoint to handle the form submission using
createConnectConfiguration.
You can preview the form while you work by following the instructions on how to develop extension UI locally.
After you add extension UI for your connector, you can remove any meta properties on the options defined using extension.addConnector. Note that leaving them in won’t result in errors.
7. Publish a private extension to test in production
Section titled “7. Publish a private extension to test in production”Once you’re ready, you can publish a temporary private extension to test in production.
Add a details.md file to your root folder with information for your user
Section titled “Add a details.md file to your root folder with information for your user”Follow the updated details on how to prepare your extension for publishing, and add a details.md to the root of your project to provide in-app documentation. You’ll be able to preview the rendered documentation in the Netlify UI once you publish the private extension.
Publish a private extension
Section titled “Publish a private extension”Follow the steps to publish a private extension using the temporary project you set up for your migration branch.
Test your private extension
Section titled “Test your private extension”Once published, you can install the extension on your team and test.
- In the Netlify UI, navigate to the Extensions section for your team.
- Select Created by your team and find your private extension in the list.
- Select the extension and then select Install on the details page.
8. Replace your published integration with the new extension
Section titled “8. Replace your published integration with the new extension”Once you’re ready to replace your published integration with your new extension, complete the following steps:
- Merge your temporary extension migration branch into the production branch that contains the code for your published integration.
- Make sure this triggers a new production deploy.
- Once the deploy is published, your old integration will be replaced by your newly migrated extension. The new extension will be available to all teams that have it installed. Depending on what your extension does, users may need to rebuild and redeploy their projects to run the new extension.
- To avoid confusion, we recommend deleting the temporary private extension and temporary project you set up for your migration branch. You will need to uninstall the extension from any projects that you tested it with before you can delete the extension and the project.
Contact us for support
Section titled “Contact us for support”If you have questions or run into issues while migrating, please contact us through Netlify support or through the technology partner program.