Skip to content

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

We recommend that you create a temporary site on Netlify to deploy your migrated extension to. You can use this site to install and test the extension in production without impacting your current users.

  1. Create a new Git branch for your extension and push the branch to your repository.
  2. Navigate to your team in the Netlify UI and select Add a new site > Import an existing project.
  3. Follow the prompts to configure a new site based on the branch. Make sure you update the value for Branch to deploy and select the branch from step 1.
  4. Deploy the site.

As you work on the migration, you can commit and push updates to your Git repository, and then deploy the site 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

First, start by updating the required dependencies for your project.

  • Install @netlify/sdk@latest and @netlify/netlify-plugin-netlify-extension@latest

  • Update the netlify.toml file in your project to include @netlify/netlify-plugin-netlify-extension

    netlify.toml
    [[plugins]]
    package = "@netlify/netlify-plugin-netlify-extension"

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

In SDK v2, the SDK commands have been renamed from netlify-integration to netlify-extension, and we’ve made the following changes:

  • netlify-extension build does not accept a --watch argument. You should use netlify-extension dev as a replacement.
  • netlify-integration preview has been removed. You should use netlify-extension dev as a replacement.

Update the package.json in your extension project to update the SDK commands:

package.json
"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:

netlify.toml
[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 = false
functions = ".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

There are two changes to make to the configuration file:

  • Rename the integration.yaml file to extension.yaml. If the file ends in .yml, rename to extension.yml.
  • Remove integrationLevel from the extension.yaml file. 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

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 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.

src/index.ts
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

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.

src/index.ts
integration.addApiHandler('custom-name', async (event, context) => {
// ...logic
return { body: JSON.stringify({}), statusCode: 200 };
});
src/endpoints/custom-name.ts
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

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

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 site-level flows, you can specify this using createSiteConfiguration and deleteSiteConfiguration.

src/index.ts
integration.onEnable(() => {
// ... logic to run after the integration is enabled for a site or team
}
integration.onDisable(() => {
// ... logic to run before the integration is disabled for a site or team
}
src/endpoints/after-install.ts
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 site */
return Response.json({});
});
src/endpoints/before-uninstall.ts
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 site */
return Response.json({});
});

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:

src/endpoints/get-site-config.ts
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:

src/endpoints/get-site-config.ts
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

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:

src/endpoints/update-site-config.ts
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:

src/endpoints/update-site-config.ts
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

If you’re using enableTeamIntegration in your extension to create a team configuration, replace it with createTeamConfiguration.

src/endpoints/create-team-config.ts
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

If you’re using enableSiteIntegration in your extension, replace it with createSiteConfiguration and pass in the teamId.

src/endpoints/create-site-config.ts
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

If you’re using getTeamIntegration in your extension, replace it with getTeamConfiguration and add a check for configurationResponse.

src/endpoints/get-team-config.ts
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

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.

src/endpoints/update-team-config.ts
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

The following methods and fields are at end of service and should be removed from your extension:

  • Remove enableBuildEventHandlers and disableBuildEventHandlers. Build event handlers run on every site 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

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

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 sites, they are now installed once on a team and then configured on individual sites. 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 site, 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

The specifics might differ depending on what you built previously but here are some rough steps for migrating to extension UI:

  1. Read through the extension UI documentation to get a sense of how it works.
  2. Run npm create @netlify/sdk@latest -- --uiMigration to add extension UI boilerplate files to your extension.
  3. Modify the generated extension details and top-level site 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 --uiMigration wizard, you should also have surfaces generated for Connect, the visual editor, or both.
  4. If required, move any endpoints defined using addApiHandler() in your src/index.ts into individual files in the src/endpoints directory. For example, an API handler defined by addApiHandler("my-function", ...) should be moved to src/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 withNetlifySDKContext to 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.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.
  5. If your endpoints include any site-specific logic, make sure they are not invoked by surfaces that operate only on teams. For example, the extension details page surface.
  6. Finally, when you’re ready, remove your old Integration UI files including src/ui/index.ts.

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

SDK v2 adds support for developing extensions for Netlify Visual Editor along with Netlify Connect. To support both features, we’ve made a few changes to the SDK that you need to apply to your extension.

Define which feature the connector supports

All connectors must specify which Netlify features it supports. It can be Connect, the visual editor, or both.

const connector = extension.addConnector({
typePrefix: 'Example',
supports: {
connect: true, // or false if not supported
visualEditor: true, // or false if not supported
},
});

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. Sites 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,
visualEditor: false,
},
});

The following connector-specific API and fields are no longer available.

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

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

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

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

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

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:

  1. Read through the extension UI documentation to get a sense of how it works.
  2. Run npm create @netlify/sdk@latest -- --uiMigration to add extension UI boilerplate files to your extension. When you receive the prompt Does your extension have a connector?, make sure you specify yes and the boilerplate will include a surface for Connect.
  3. 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.
  4. 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

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

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

Follow the steps to publish a private extension using the temporary site you set up for your migration branch.

Test your private extension

Once published, you can install the extension on your team and test.

  1. In the Netlify UI, navigate to the Extensions section for your team.
  2. Select Created by your team and find your private extension in the list.
  3. Select the extension and then select Install on the details page.

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:

  1. Merge your temporary extension migration branch into the production branch that contains the code for your published integration.
  2. Make sure this triggers a new production deploy.
  3. 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 sites to run the new extension.
  4. To avoid confusion, we recommend deleting the temporary private extension and temporary site you set up for your migration branch. You will need to uninstall the extension from any sites that you tested it with before you can delete the extension and the site.

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.

Got it!

Your feedback helps us improve our docs.