KDD Brands → Store 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-integrationand Cloudflare Workers Logs. - Companion docs
- Privacy policy (what is stored and why) · Security & incidents
- Last reviewed
- 2026-07-26
1. Retention schedule
| Data | Store | Retention | Enforced 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) |
SurveyResponse — customerId,
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 emails — ALERT: 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
app/uninstalled→DisableShopJob→ deletesSessionrows for the shop and setsBillingSchedule.active = false. Nothing else is removed. Deliberate: Shopify sendsshop/redact48 hours later, and a merchant who reinstalls in the meantime keeps their configuration.customers/redact(and, today,customers/data_request) → deletes matchingSurveyResponseandEmailLogrows only, because those are the tables holding customer-identifiable data.shop/redact→DeleteBillingScheduleJob→ 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
JobQueuerows are kept forever by design; they can carry order and contract ids. They are removed only byshop/redactor by hand. ContractActivityoutlives a customer redaction. That is defensible — the table holds no customer or order identifier — but it does mean a contract’s timeline survives acustomers/redactfor 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