前言

Shopify App Store distribution 的 App 必須處理三類 mandatory compliance webhooks:顧客資料查閱、顧客資料刪除與商店資料刪除。第一版最容易完成的是 route、HMAC authentication 與 HTTP 200;但 200 只代表 Shopify 成功把請求交給 App,不代表資料已匯出、交付或刪除。

上一篇談的是 商家 UI 與 developer diagnostics 的責任分離。Compliance request 也需要相同觀念:Webhook transport、App 內處理、營運交付與稽核紀錄是不同責任,不能用一個綠色狀態全部概括。

這篇是工程案例與 2026-07-28 Shopify 官方文件的交叉整理,不是法律意見;若所在地法規、商家合約或 Shopify 後續規則更嚴格,應採更嚴格要求。

原本的做法與問題症狀

最初的三個 route 已經會:

  1. 接受 Shopify POST。
  2. 使用 framework helper 驗證 Webhook。
  3. 記錄 topic 與 shop。
  4. 回傳 200。

這能通過「endpoint 存在」的最低 transport 測試,卻留下三個實質問題:

  • customers/data_request 沒有 durable record,回 200 後營運方甚至不知道還有待交付資料。
  • customers/redact 沒有從 local sync jobs/logs 找出相關 customer 或 order 資料。
  • shop/redact 沒有清除解除安裝商店的 session、connection 與同步狀態。

另一個看似省事的方向是收到 data request 就自動寄出 payload。這會把未驗證的 Email、敏感 JSON、公開下載連結與 mail delivery failure 全部帶進來。案例因此先採「App 內產生受限匯出+人工確認交付」。

問題是怎麼推敲出來的?

Git history 顯示功能按責任逐步補齊:先加 subscriptions/routes,再加入 local erasure、data request persistence、local export、delivery tracking,最後用 runbook 與 readiness gate明確標出外部交付責任。

現行 Shopify 文件也把三個 topic 分得很清楚:

  • customers/data_request 提供 App 需要交給 store owner 的 customer/order resource IDs。
  • customers/redact 要刪除對應顧客資料。若顧客過去六個月沒有下單,Shopify 在刪除請求十天後送出;有近期訂單時,請求會延後到六個月期滿。
  • shop/redact 在商店擁有者解除安裝 App 48 小時後送出,要求清除該 shop 資料。

時間點是 Shopify 何時送 delivery,不等於 App 可以再等待相同天數才開始處理。App 仍要定義內部 SLA、稽核與 escalation。

最後採用的架構

案例把三條流程拆開:

Shopify compliance webhook
  → HMAC authentication
  → topic-specific local action
  → HTTP 200

customers/data_request
  → upsert pending request
  → generate local-only export
  → identity / scope review
  → secure external delivery
  → delivered timestamp + note

customers/redact
  → parse customer / order identifiers
  → delete matching same-shop sync jobs and logs

shop/redact
  → delete same-shop sessions, connection and sync state

Data request export 只包含 App 自己保存的資料,不重新向 Shopify 或外部資料系統抓取,也不冒充商家的完整資料副本。交付前要確認請求人、範圍與安全通道;不能把 payload 裡的 Email 當成已驗證收件人。

關鍵實作

Route 先使用 Shopify template 的 authentication helper:

export async function action({ request }: ActionArgs) {
  const { payload, shop } = await authenticate.webhook(request);

  return handleComplianceWebhook({
    shop,
    topic: 'customers/data_request',
    payload,
  });
}

Data request 有平台 request ID 時,以 shop+topic+request ID upsert,避免同一請求重送後建立多筆待辦:

await complianceRequests.upsert({
  key: { shop, topic, platformRequestId },
  create: { status: 'pending', identifiers, payloadSnapshot },
  update: {
    status: 'pending',
    identifiers,
    payloadSnapshot,
    export: null,
    deliveredAt: null,
  },
});

交付狀態不能跳過 export:

if (!request.completedAt || !request.exportJson) {
  throw new Error('請先產生資料匯出');
}

await markDelivered({
  requestId: request.id,
  deliveredAt: new Date(),
  note: safeDeliveryNote,
});

Customer redaction 依 resource IDs 與必要 tokens 比對同 shop rows;Shop redaction 則以 shop 為 boundary 刪除多類 local rows。公開文章不放真實 payload、Email、Phone、order ID、shop domain 或 delivery note。

實際驗證

來源 repository 的測試可證明:

  • 三個 route 都先執行 Webhook authentication,再交給 shared handler。
  • 三個 topic 都有對應 subscription 與 route。
  • Data request 會保存 pending record,以及 customer/order identifiers。
  • 同 shop、同 topic、同 request ID 重送時更新同一筆,不建立重複 request。
  • Data request 不會誤刪同步資料。
  • Export 只包含同 shop、符合 identifiers 的 local sync jobs/logs。
  • 未產生 export 時不能標記 delivered。
  • 另一個 shop 不能匯出或標記本 shop 的 request。
  • Customer redaction 只刪除同 shop 的 matching rows。
  • Shop redaction 的測試會保留其他 shop 資料。

這些仍是 repository 內測試,不是正式 Shopify delivery rehearsal。案例沒有可公開的 Dev Dashboard delivery log、長時間 retention test 或外部交付演練結果。

仍然存在的限制

第一,shared handler 在回 200 前同步執行資料庫 upsert、掃描與 delete。Shopify 的 HTTPS Webhook 整體 request timeout 是五秒;資料量變大時,customer redaction 的逐列比對可能超時。更穩健的方向是先持久化帶有 delivery ID 的 compliance job,再快速 ACK,由受控 worker 完成可重試處理。

第二,案例以 platform data request ID 去重 customers/data_request,但沒有保存 X-Shopify-Webhook-Idcustomers/redactshop/redact 多半可重入,仍應保存 delivery ID、attempt 與結果,才能回答某次 delivery 是否完成。

第三,customer matching 會在 JSON snapshot 中找 identifiers。這能覆蓋早期未正規化資料,卻可能因格式差異漏掉,或因字串碰巧相同而刪錯。長期應把 customer/order identity 正規化成可索引欄位,並做 deletion inventory。

第四,shop redaction 目前明確刪除 session、connection、sync settings/jobs/logs 與 AI mapping setting;僅靠這段函式與文件還不能證明所有 schema tables、backup、log sink、cache 與第三方 processor 都已清除。正式上線需要 data inventory、foreign-key/cascade review、backup retention policy 與 integration test。

第五,delivered 是人工紀錄,不是加密檔案傳輸的技術證明。若未來加入 Email 或下載連結,還要處理身份驗證、有效期限、access log、bounce、重試與撤銷。

最後,Webhook payload 有 API version。Shopify 建議每季更新到 latest stable 並在更新前測試 handler;不能設定一次 compliance topics 就假設 payload 永遠不變。

可以延伸到哪些情境?

Privacy request 最重要的是把狀態拆清楚:

received → authenticated → persisted → processing
→ completed → delivered / redacted → audited

這套狀態也適用 ERP、CRM、會員中心與資料倉儲。每個 processor 都要能回答保存了哪些資料、如何找出、誰能交付、如何刪除、如何處理重送,以及哪一項仍需要人工或法律確認。

本批上線治理文章到這裡完成:Readiness CLI 阻擋可自動判斷的缺口、商家 App Home 只顯示可操作狀態,而 compliance workflow 則把收到、完成與交付拆成可追蹤步驟。

參考資料

以上平台文件查核日期:2026-07-28。