KDD Brands

KDD BrandsStore Integration → Data retention

Data Retention Policy — Store Integration

This policy states the retention period actually implemented in code, and names the code path that enforces it. Where retention is “indefinite until an event”, it says so rather than inventing a period the app does not enforce.

Scope
Every persistent store the app controls — the Cloudflare D1 database kdd-app-integration and Cloudflare Workers Logs.
Companion docs
Privacy policy (what is stored and why) · Security & incidents
Last reviewed
2026-07-26

1. Retention schedule

DataStoreRetentionEnforced by
Session — merchant OAuth session, incl. accessToken, staff name/email D1 Until the app is uninstalled, then deleted. Also deleted on shop/redact. Rows carry an expires value for online tokens, honoured by the session storage library. app/jobs/shop/DisableShopJob.ts:23-26 (on app/uninstalled); app/jobs/shop/DeleteBillingScheduleJob.ts (on shop/redact)
SurveyResponsecustomerId, orderId, orderName, choice, otherText (buyer free text) D1 Indefinite while the shop is installed. Deleted on customers/redact (matching customer or order), on shop/redact, or when the merchant deletes the survey from the admin. No age-based expiry is implemented. app/routes/webhooks.customer_redact.ts:45-60; app/jobs/shop/DeleteBillingScheduleJob.ts
EmailLog — sent-notification history: customerId, orderId, contractId, template, channel, status (no name, email address or message body) D1 Indefinite while the shop is installed. Deleted on customers/redact (matching customer or order) and on shop/redact. No age-based expiry is implemented. app/routes/webhooks.customer_redact.ts:56-59; app/jobs/shop/DeleteBillingScheduleJob.ts
ContractActivity — subscription timeline (contractId, type, summary, detailJson, actor) — pseudonymous, holds no customer identifiers by design D1 Indefinite while installed; deleted on shop/redact only. It is deliberately not touched by customers/redact, because it carries nothing that can be matched to a buyer. app/jobs/shop/DeleteBillingScheduleJob.ts
DunningTracker — failed-billing-cycle markers (pseudonymous) D1 Indefinite while installed; deleted on shop/redact. Rows are marked complete (completedAt) rather than deleted once dunning ends, because completion is what stops a retry from firing again. app/jobs/shop/DeleteBillingScheduleJob.ts
JobQueue — delayed job rows, parametersJson may contain shop/contract/order ids D1 Deleted when the job runs (normal case: minutes to hours). Rows that exhaust MAX_ATTEMPTS are kept indefinitely with deadLettered = true for inspection and manual resurrection. Deleted on shop/redact. Sweep/execute path (SweepDueJobsJob); dead-letter behaviour documented at prisma/schema.prisma:58-60; app/jobs/shop/DeleteBillingScheduleJob.ts
BillingSchedule — per-shop billing hour/timezone D1 Deactivated (active = false) on uninstall, deleted on shop/redact. app/jobs/shop/DisableShopJob.ts:25; app/jobs/shop/DeleteBillingScheduleJob.ts
Survey, TrackingSettings, CarrierAlias, PostPurchaseFlow, CheckoutDiscountRule — merchant configuration, no personal data D1 Life of the install. Survives uninstall (so a reinstall inside Shopify’s 48-hour window is not destructive), then deleted on shop/redact. Merchant can delete any record at any time from the app admin. app/jobs/shop/DeleteBillingScheduleJob.ts
Cloudflare Workers Logs — application logs, incl. the customer email lines noted in the privacy policy §8.2 Cloudflare Cloudflare’s Workers Logs retention (days, not months — set by the Cloudflare plan, not by this app). Not independently exported or archived by the app. wrangler.toml:27-28 ([observability] enabled = true)
Ops alert emailsALERT: notifications to the operator Operator mailbox Contain shop domain, job name and error text; no customer identifiers by design. Retained per the operator’s mailbox policy. app/utils/alerts.server.ts, [[send_email]] in wrangler.toml
Data sent to Klaviyo (opt-in shops only) Klaviyo Out of this app’s control once sent. Governed by the merchant’s own Klaviyo account and retention settings. app/lib/integrations/klaviyo.server.ts:144-148

2. The three deletion triggers, precisely

  1. app/uninstalledDisableShopJob → deletes Session rows for the shop and sets BillingSchedule.active = false. Nothing else is removed. Deliberate: Shopify sends shop/redact 48 hours later, and a merchant who reinstalls in the meantime keeps their configuration.
  2. customers/redact (and, today, customers/data_request) → deletes matching SurveyResponse and EmailLog rows only, because those are the tables holding customer-identifiable data.
  3. shop/redactDeleteBillingScheduleJob → deletes all twelve shop-scoped tables. This is the complete erasure path.

All three handlers verify Shopify’s HMAC via authenticate.webhook() before touching data, so deletion cannot be triggered by an unauthenticated caller.

3. Backups

The app performs no application-level backups and keeps no separate copy of D1. Cloudflare’s own D1 point-in-time recovery (time-travel) applies at the platform level; a restore would reintroduce deleted rows, so any restore must be followed by re-running the outstanding redactions. This is a manual step and is listed in our internal incident-response runbook.

4. Gaps in this policy

Stated so a reviewer does not have to discover them:

  • No age-based expiry anywhere. Survey responses and notification logs in particular are kept for the life of the install. A merchant who wants a 12- or 24-month ceiling has to delete the records. A scheduled pruning job is the obvious fix and is not built.
  • Dead-lettered JobQueue rows are kept forever by design; they can carry order and contract ids. They are removed only by shop/redact or by hand.
  • ContractActivity outlives a customer redaction. That is defensible — the table holds no customer or order identifier — but it does mean a contract’s timeline survives a customers/redact for the buyer on that contract.
  • Log retention is Cloudflare’s, not ours, and the app does not currently scrub the email addresses the Klaviyo bridge writes into those logs (privacy policy §8.2).

5. Review

This policy is reviewed whenever prisma/schema.prisma gains a new model. Any new shop-scoped model must be added to DeleteBillingScheduleJob.perform() and to the table in §1, or the app silently stops being redact-complete. The test app/jobs/shop/tests/DeleteBillingScheduleJob.test.ts (“purges every shop-scoped record”) exists to make that omission fail loudly.


Last reviewed 2026-07-26 · KDD Brands LTD (company 16411950) · privacy@kddbrands.com