Uninstall webhook not delivering on dev app

Hello all,

I’m having an issue where my app’s uninstall webhook. I’m not sure if it’s because my app isn’t currently public, I copied the implementation of my other public app that I’ve tested and it works on that app. The behavior I’m expecting is a log from my server saying “webhook delivery response 200” to fire when I uninstall my app on my development store. Currently, nothing happens when I uninstall the app. No logs, no updates to my database. It’s as though the webhook is just not firing.

NOTE: If uninstall webhooks don’t fire on development stores or development/non-public apps then that answers my question. If not-

I have a webhook subscription in my TOML file:
[[webhooks.subscriptions]] uri = "https://APP_NAME.herokuapp.com/webhooks" topic = "APP_UNINSTALLED"

I also have this webhook section in my shopify.server.js file:

webhooks: {
    APP_UNINSTALLED: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    CUSTOMERS_DATA_REQUEST: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    CUSTOMERS_REDACT: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
    SHOP_REDACT: {
      deliveryMethod: DeliveryMethod.Http,
      callbackUrl: "/webhooks",
    },
  },
  hooks: {
    afterAuth: async ({ session }) => {
      await shopify.registerWebhooks({ session });
    },
  },
  future: {
    unstable_newEmbeddedAuthStrategy: true,
    removeRest: true,
    v3_webhookAdminContext: true,
  },
  ...(process.env.SHOP_CUSTOM_DOMAIN
    ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
    : {}),
});
...
export const registerWebhooks = shopify.registerWebhooks;

I tried manually firing an uninstalled webhook and it successfully delivered (so endpoint is correct).

Here’s my webhook file in case it’s that (unlikely, it’s delivering fine)

import { authenticate } from "../shopify.server";
import db from "../db.server";
import { Prisma } from "@prisma/client";

export const action = async ({ request }) => {
    const { topic, shop, session, admin, payload } = await authenticate.webhook(
        request
    );

    console.log(`Received webhook: ${topic} for shop: ${shop}`);
    console.log('Request headers:', Object.fromEntries(request.headers.entries()));

    if (!admin && topic !== "APP_UNINSTALLED") {
        console.log("No admin context available for non-uninstall webhook");
        throw new Response();
    }

    switch (topic) {
        case "APP_UNINSTALLED":
            console.log("Processing APP_UNINSTALLED webhook");
            try {
                if (session) {
                    console.log("Session found, looking up user");
                    const user = await db.user.findUnique({
                        where: { shopDomain: shop }
                    });

                    if (user?.pixelId) {
                        console.log(`Deleting web pixel with ID: ${user.pixelId}`);
                        await admin.graphql(
                            `mutation {
                                webPixelDelete(id: "${user.pixelId}") {
                                    userErrors {
                                        code
                                        field
                                        message
                                    }
                                }
                            }`
                        );
                    }

                    console.log("Updating user and deleting sessions");
                    await Promise.all([
                        db.session.deleteMany({ where: { shop } }),
                        db.user.update({
                            where: { shopDomain: shop },
                            data: {
                                deletedAt: new Date(),
                                pixelId: null
                            }
                        })
                    ]);
                    console.log("Uninstall cleanup completed successfully");
                }
                return new Response(null, { status: 200 });
            } catch (error) {
                if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
                    console.log("No records found to delete - this is okay");
                    return new Response(null, { status: 200 });
                } else {
                    console.error("Error during APP_UNINSTALLED webhook:", error);
                    return new Response("Internal server error", { status: 500 });
                }
            }
        case "CUSTOMERS_DATA_REQUEST":
            console.log("Received CUSTOMERS_DATA_REQUEST webhook");
            return new Response("We don't collect customer data.", { status: 200 });
        case "CUSTOMERS_REDACT":
            console.log("Received CUSTOMERS_REDACT webhook");
            return new Response(null, { status: 200 });
        case "SHOP_REDACT":
            console.log("Processing SHOP_REDACT webhook");
            await db.session.deleteMany({ where: { shop } });
            return new Response(null, { status: 200 });
        default:
            console.log(`Received unhandled webhook topic: ${topic}`);
            return new Response("Unhandled webhook topic", { status: 404 });
    }
};

It’s just when I uninstall the app on one of my development stores, it doesn’t fire the uninstalled webhook. I really don’t know what I did to make this not work. Help appreciated!!

I figured out the issue.

For posterity, the issue was that I uninstalled it once and the uninstall webhook presumably ran into an error where it made an invalid prisma call before deleting the user session which made it so the uninstall webhook registered as firing correctly but the session never got deleted. So when I reinstalled the app, it thought that my store never uninstalled the app so it never re-registered the webhooks. The Shopify webhook template calls afterAuth when creating a new session because the logic is if a user doesn’t have a session then they must have just installed the app so it calls the authentication (and afterAuth) flow.

The fix was to uninstall the app and manually delete the user session. Then when I installed it again, it didn’t see a session and realized that my store just installed the app and registered for webhooks.

2 Likes

Hey Beckett! Glad you figured this out and appreciate you posting your solution - that will help devs who encounter this issue in the future!

1 Like