Privacy Law Compliance Webhooks in Remix App - Trouble with Configuration

Privacy Law Compliance Webhooks in Remix App - Trouble with Configuration

byteware
Shopify Partner
1 0 0

Problem:

I’m building a Shopify app using Remix and have been struggling to properly configure the required Privacy Law Compliance Webhooks. The documentation is somewhat inconsistent, and I’m unsure how to properly register and handle these webhooks.

 

What I’ve Noticed:

  • The examples in the official docs show different ways to register compliance webhooks.
  • It’s unclear if I can combine regular webhooks and compliance webhooks in one config block.
  • There’s no confirmation whether these compliance webhooks were successfully registered.

Current shopify.app.toml Configuration:

 

# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration

client_id = "05285bcaafb1760bd6804d389165f97e"
name = "test-subscription"
handle = "test-subscription"
application_url = "https://watches-transport-boulder-generally.trycloudflare.com"
embedded = true

[build]
automatically_update_urls_on_dev = true
include_config_on_deploy = true

[webhooks]
api_version = "2025-04"

  [[webhooks.subscriptions]]
  topics = [ "app/uninstalled" ]
  uri = "https://working-photographer-college-race.trycloudflare.com/webhooks/app/uninstalled"

  [[webhooks.subscriptions]]
  topics = [ "customers/create" ]
  uri = "https://working-photographer-college-race.trycloudflare.com/webhooks/customers/create"

[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "read_customers,read_orders,read_products,write_customers,write_orders,write_products"

[auth]
redirect_urls = [
  "https://watches-transport-boulder-generally.trycloudflare.com/auth/callback",
  "https://watches-transport-boulder-generally.trycloudflare.com/auth/shopify/callback",
  "https://watches-transport-boulder-generally.trycloudflare.com/api/auth/callback"
]

[pos]
embedded = false

 

Webhook Handler (Remix):

 
import { ActionFunctionArgs, json } from "@remix-run/node";
import { authenticate } from "../shopify.server";

/**
 * Central Webhook Handler for Shopify
 * 
 * This route processes both regular webhooks and required compliance webhooks:
 * 
 * Regular Webhooks:
 * - app/uninstalled: Triggered when the app is uninstalled
 * - customers/create: Triggered when a new customer is created
 * 
 * Compliance Webhooks:
 * - customers/data_request: Requests to view stored customer data
 * - customers/redact: Requests to delete customer data
 * - shop/redact: Requests to delete shop data
 */
export const action = async ({ request }: ActionFunctionArgs) => {
try {
// HMAC verification is automatically performed by authenticate.webhook
// If verification fails, the function throws an error which is returned as a 401
const { topic, shop, session, payload } = await authenticate.webhook(request);

console.log(`Webhook received: ${topic} for shop ${shop}`);
console.log(`Payload: ${JSON.stringify(payload, null, 2)}`);

switch (topic) {
// Regular Webhooks
case "app/uninstalled":
await handleAppUninstalled(shop, session, payload);
break;

case "customers/create":
await handleCustomerCreate(shop, session, payload);
break;

// Compliance Webhooks
case "customers/data_request":
await handleCustomersDataRequest(shop, session, payload);
break;

case "customers/redact":
await handleCustomersRedact(shop, session, payload);
break;

case "shop/redact":
await handleShopRedact(shop, session, payload);
break;

default:
console.warn(`Unknown webhook topic: ${topic}`);
}

// Return 200 OK to confirm that the webhook was received
return json({ success: true });
} catch (error) {
console.error(`Error processing webhook:`, error);
// Log error details for better diagnosis
if (error instanceof Error) {
console.error("Error message:", error.message);
console.error("Stack:", error.stack);
}
// Return 200 despite error to acknowledge the request
// We will handle the error later
return json({ success: true });
}
};

/**
 * Handles app uninstallation
 */
async function handleAppUninstalled(shop: string, session: any, payload: any) {
console.log('App was uninstalled from shop:', shop);

// TODO: Implementation for your app
// 1. Clean up all shop-related data
// 2. Remove permissions or tokens
// 3. Update database status
}

/**
 * Handles the creation of a new customer
 * Code integrated from webhooks.customers.create.ts
 */
async function handleCustomerCreate(shop: string, session: any, payload: any) {
console.log("📣 Processing webhook for customers/create");
console.log(`🏪 Shop: ${shop}`);

try {
// Format data for the external API
const userData = {
user: {
id: payload.id?.toString() || "",
email: payload.email || "",
first_name: payload.first_name || "",
last_name: payload.last_name || "",
source: "shopify",
created_at: payload.created_at || new Date().toISOString()
}
};

console.log("📤 Sending data to webhook.site:", JSON.stringify(userData, null, 2));

// Send data to webhook.site (for testing purposes)
const response = await fetch("https://webhook.site/3d67bbe9-3b68-4c98-a11c-d33a25373074", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});

const responseText = await response.text();
console.log("📩 Webhook.site status:", response.status);
console.log("📩 Webhook.site response:", responseText);

// TODO: Implement your own business logic here
// e.g., storing customer data in the database

} catch (error) {
console.error("❌ Error processing customer-create webhook:", error);
// Log error but continue execution
// Webhook handlers should not throw errors
}
}

/**
 * Processes requests to view customer data
 * 
 * Example payload:
 * {
 * "shop_id": 954889,
 * "shop_domain": "shop-name.myshopify.com",
 * "orders_requested": [299938, 280263, 220458],
 * "customer": {
 * "id": 191167,
 * "email": "john@example.com",
 * "phone": "555-625-1199"
 * },
 * "data_request": {
 * "id": 9999
 * }
 * }
 */
async function handleCustomersDataRequest(shop: string, session: any, payload: any) {
console.log('Data Request received for shop:', shop);

// TODO: Implementation for your app
// 1. Collect all relevant customer data for the specified customer.id or customer.email
// 2. Create a complete summary of all data your app stores
// 3. Provide this data to the merchant through a secure method
// (e.g., encrypted download link, API response, or email notification)

// If you use a database, you would run queries here to gather all customer data

// Example for logging purposes
console.log('Data requested for customer:', payload.customer);

// In a complete implementation, you would retrieve all stored data here
// and send it to the merchant
}

/**
 * Processes requests to delete customer data
 * 
 * Example payload:
 * {
 * "shop_id": 954889,
 * "shop_domain": "shop-name.myshopify.com",
 * "customer": {
 * "id": 191167,
 * "email": "john@example.com",
 * "phone": "555-625-1199"
 * },
 * "orders_to_redact": [299938, 280263, 220458]
 * }
 */
async function handleCustomersRedact(shop: string, session: any, payload: any) {
console.log('Customer Redact Request received for shop:', shop);

// TODO: Implementation for your app
// 1. Identify all stored data for the specified customer
// 2. Delete or anonymize this data
// 3. Document the deletion for compliance verification

// Example for logging purposes
console.log('Deletion requested for customer:', payload.customer);

// In a complete implementation, you would delete all customer data here
// e.g., with Prisma (if your app uses it):
// await prisma.customerData.deleteMany({
// where: {
// customerId: payload.customer.id.toString(),
// },
// });
}

/**
 * Processes requests to delete shop data
 * 
 * Example payload:
 * {
 * "shop_id": 954889,
 * "shop_domain": "shop-name.myshopify.com"
 * }
 */
async function handleShopRedact(shop: string, session: any, payload: any) {
console.log('Shop Redact Request received for shop:', shop);

// TODO: Implementation for your app
// 1. Identify all data stored for the entire shop
// 2. Delete or anonymize this data
// 3. Document the deletion for compliance verification

// Example for logging purposes
console.log('Deletion requested for shop:', payload.shop_domain);

// In a complete implementation, you would delete all shop data here
// e.g., with Prisma (if your app uses it):
// await prisma.shopData.deleteMany({
// where: {
// shopDomain: payload.shop_domain,
// },
// });

// If your app stores files, these should also be deleted
}

 

Questions:

1. Do webhook URIs need to be absolute URLs or can they be relative paths in the shopify.app.toml config?

2. Can compliance and regular webhooks be defined together, or should they be kept in separate blocks?

3. How can I confirm if compliance webhooks are successfully registered with Shopify?

4. Do I need a separate route handler for each compliance webhook, or can one shared handler handle all three topics?

5. Is there a specific API version required for compliance webhooks?

6. Has anyone implemented these compliance webhooks in a Remix app using Shopify CLI 3.0? Any best practices or working examples?

Replies 0 (0)