Skip to main content

How to Update App Webhooks

Problem

Imagine you have an app that notifies an external service whenever a new order is created. The app registers the ORDER_CREATED webhook based on the subscription webhook payload from the OrderCreatedSubscription.graphql file:

# OrderCreatedSubscription.graphql
subscription OrderCreated {
event {
... on OrderCreated {
order {
id
created
number
}
}
}
}

The subscription defines what data will be sent to the app along with the webhook. The app can then execute logic utilizing the subscription payload. In our case, it will be notifying the external service about the new order:

// pseudo-code for order-created webhook handler

const payload = {
id: order.id,
created: order.created,
number: order.number,
};

service.notifyOrderCreated(payload);

After some time, you realize you need an additional order field: status. In the next app release, you add it to the subscription webhook payload:

# OrderCreatedSubscription.graphql
subscription OrderCreated {
event {
... on OrderCreated {
order {
id
created
number
status
}
}
}
}

You modify the code to use the new field:

// pseudo-code for order-created webhook handler

const payload = {
id: order.id,
created: order.created,
number: order.number,
status: order.status,
};

service.notifyOrderCreated(payload);

You deploy the app and trigger the ORDER_CREATED webhook. Perhaps surprised, you see a type error: order.status is not defined. What happened?

Although you did modify both the OrderCreatedSubscription.graphql file, and the code of your application, Saleor still uses the original subscription from the app installation manifest.

caution

Whenever your app starts using a new field from the subscription, you must update the app's webhook query field.

If your app hasn't yet been released or does not require zero downtime, the easiest solution would be to simply reinstall it. The query used in the subscription will be regenerated during the installation.

However, if your app has to stay functional at all times, you have to programmatically update its webhooks.

Webhook migration script

To update webhooks without disrupting service you could use a webhook migration script. Those scripts should, ideally, execute before your app deployment (e.g., in CI). This way, you can ensure the app has all the required subscription fields without downtime.

If you are using @saleor/app-sdk for app development, you can use some of its helpers in your migration script.

Here is what a webhook migration script may look like:

1. Authenticate app

To authenticate app-related API calls, you need to get the app token from its authData.

Where you get authData will depend on your APL implementation.

Let's assume you are using the UpstashAPL provided in app-sdk:

import { UpstashAPL } from "@saleor/app-sdk/APL";

export const getAppAuthData = async () => {
// Requires `UPSTASH_URL` and `UPSTASH_TOKEN` environment variables
// Initialize UpstashAPL
const apl = new UpstashAPL();

// Get authData of all registered apps
const apps = await apl.getAll();

// Assuming there is only one app, return its authData
return apps[0];
};

2. Get a webhook manifest

We will update our webhook with webhookUpdate mutation. As its input, we only want to pass the new value of the query field.

We can get a stringified query field from the webhook manifest. Webhook manifest is the result of executing getWebhookManifest method on the instance of SaleorAsyncWebhook or SaleorSyncWebhook classes. These classes are provided by the @saleor/app-sdk to help you build your webhooks for the app manifest.

Here is what webhook handler may look like for an ORDER_CREATED webhook:

// api/webhooks/order-created.ts

import { saleorApp } from "@/saleor-app";
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
OrderCreatedDocument,
OrderCreatedSubscriptionPayloadFragment,
} from "@/generated/graphql";

export const orderCreatedWebhook =
new SaleorAsyncWebhook<OrderCreatedSubscriptionPayloadFragment>({
name: "Order Created",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CREATED",
apl: saleorApp.apl,
query: OrderCreatedDocument, // OrderCreatedDocument will be converted into a string query
});

You can see getWebhookManifest being used in the webhooks field of your manifest.ts, where the app manifest is created. Saleor registers the app based on this manifest.

// pages/api/manifest.ts
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
import { AppManifest } from "@saleor/app-sdk/types";
import { orderCreatedWebhook } from "./webhooks/order-created";

export default createManifestHandler({
async manifestFactory({ appBaseUrl }) {
const manifest = {
// ...
webhooks: [orderCreatedWebhook.getWebhookManifest(appBaseUrl)],
};
return manifest;
},
});

We will repeat the same logic in our migration script to get the current state of our webhook:

import { orderCreatedWebhook } from "./pages/api/webhooks/order-created";

const runMigration = async () => {
const authData = await getAppAuthData();

// Regenerate orderCreated webhook manifest with updated state
const webhookManifest = orderCreatedWebhook.getWebhookManifest(
authData.saleorApiUrl
);
};

3. Update the webhook

note

In the next code example, we will assume the existence of:

  • createGraphQLClient - a function that returns a GraphQL client
  • AppWebhookManager - a class that takes in the GraphQL client and makes calls to the Saleor API. It has the following methods:
    • updateWebhookQuery - runs webhookUpdate mutation with query input
    • getWebhookByName - runs app query and returns one of the webhooks by its name

Once we have the up-to-date manifest, we can retrieve the stringified query from it and update the webhook:

const runMigration = async () => {
// ...

// Imaginary function that creates a GraphQL client for your API calls. This can be Apollo Client, Urql Client, etc.
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});

// Imaginary class that takes in GraphQL client and makes calls to the Saleor API
const appWebhookManager = new AppWebhookManager({
client,
});

// Get existing webhook we want to update
const webhookToUpdate = await appWebhookManager.getWebhookByName({
name: webhookManifest.name,
});

// Update webhook with new query
const webhooks = await appWebhookManager.updateWebhookQuery({
id: webhookToUpdate.id,
query: webhookManifest.query, // update webhook with fields from the new manifest
});
};
Expand ▼

Next steps

For most cases, the above script should be enough to update the webhook. However, if you can't afford to have any downtime, you should consider a more complex migration process that factors in:

  • Rollback - If the migration fails, you should be able to roll back to the previous state. This can be achieved by deactivating the old webhook, creating a new webhook, testing it, and only then removing the old one.
  • Queued events - Saleor puts events in a queue and processes them asynchronously. This means that even if you update the webhook, the events that were put in the queue before will still be sent with the old query. Your app should be able to work with both old and new queries until the migration is complete.
  • Safe query modification - If you add a new field to the query, you should ensure that the app can handle the absence of this field in the old events. If you remove a field, you should ensure that the app can handle the presence of this field in the old events.

If you need the full-picture view of migration scripts, feel free to peak into saleor/apps repository.