KDD Brands

KDD BrandsStore Integration → Privacy

Privacy Policy — Store Integration

This document describes what Store Integration actually does with personal data. It is written from the code, not from a template. Where a control is weak or incomplete it says so — §8 lists the known gaps.

App
Store Integration (Shopify app, client_id 6a455a2111c712d240207233490aae62)
Operator
KDD Brands LTD, registered in England and Wales, company number 16411950.
Data controller for app-operational data.
Registered office
71–75 Shelton Street, Covent Garden, London WC2H 9JQ, United Kingdom
Privacy contact
privacy@kddbrands.com
Last reviewed
2026-07-26

1. Roles

  • The merchant (the Shopify store that installs the app) is the controller of their customers’ personal data.
  • Store Integration is a processor acting on the merchant’s instructions, and a controller only for the merchant’s own account/staff data needed to operate the app (see Session below).
  • Shopify remains the system of record. Subscription contracts, orders, customers, addresses and payment mandates live in Shopify, not in this app.

2. What the app stores

All persistent storage is a single Cloudflare D1 database (binding DB, database kdd-app-integration; see wrangler.toml:30-35). The schema is prisma/schema.prisma. Every table below is scoped by shop (the merchant’s *.myshopify.com domain).

2.1 Tables containing personal data

TablePersonal data columnsWhose dataWhy it is stored
Session
(prisma/schema.prisma:13-29)
accessToken, userId, firstName, lastName, email, locale, accountOwner, collaborator, emailVerified, scope, expires Merchant staff / store owner Required OAuth session storage for @shopify/shopify-app-remix. accessToken is a credential, not merely personal data, and is the most sensitive value the app holds.
SurveyResponse
(prisma/schema.prisma:200-218)
customerId (numeric Shopify customer id from the session token sub claim), orderId, orderName (e.g. #1042), choice, otherText (free text typed by the buyer — can contain anything they type) Merchant’s customers Post-purchase survey answers (“how did you hear about us?”), shown back to the merchant as first-party attribution.
EmailLog
(prisma/schema.prisma:258-271)
customerId, orderId, contractId, plus template, channel, status, reason. No name, email address or message body. Merchant’s customers — pseudonymous, but keyed to a buyer via customerId/orderId Sent-notification history: one row per customer-facing notification the app triggers, so the merchant can see what was attempted and whether it was emitted, skipped or failed.
ContractActivity
(prisma/schema.prisma:233-247)
contractId, type, summary, detailJson, actor Pseudonymous — identifies a subscription contract, not a named person Subscription timeline shown to the merchant (pauses, quantity changes, billing attempts). Holds no customer identifiers and no buyer-typed text by design (app/lib/activity/log.server.ts).
DunningTracker
(prisma/schema.prisma:273-284)
contractId, billingCycleIndex, failureReason Pseudonymous — identifies a subscription contract, not a named person Prevents duplicate dunning/retry actions for the same failed billing cycle. Contains no name, email, address or card data, but is linkable to a person via Shopify.
JobQueue
(prisma/schema.prisma:45-64)
parametersJson — the serialised parameters of a delayed job, which may transiently include a shop domain, a subscription contract id or an order id Pseudonymous Durable retry/scheduling of delayed work. Rows are deleted when the job runs (or flagged deadLettered on repeated failure — see §5).

2.2 Tables containing merchant configuration only (no personal data)

BillingSchedule, Survey, TrackingSettings, CarrierAlias, PostPurchaseFlow, CheckoutDiscountRule. These hold settings the merchant types into the app admin — plan hours and timezone, survey questions and choice lists, tracking page copy and carrier display names, offer/discount rules. They contain no data about identifiable customers.

2.3 What the app deliberately does not store

  • No order contents, customer names, addresses, phone numbers or email addresses are persisted. The branded tracking page (app/lib/tracking/lookup.server.ts) queries the Shopify Admin API live on each lookup, compares the supplied email or postcode in memory, and returns a masked result. Nothing from that query is written to D1.
  • No card or bank data, ever. Payment mandates are Shopify’s; the app can only ask Shopify to send the customer a payment-update email (app/services/SendPaymentMethodUpdateEmailService.ts).
  • No analytics/tracking pixels, no advertising identifiers, no cross-merchant profiling. Data is never combined across merchants and is never sold.

3. Where the data is, and how it is protected

  • At rest: Cloudflare D1. Cloudflare encrypts D1 data at rest. The app adds no application-level (field) encryption — including on Session.accessToken. This is a known weakness and is recorded as such in §8.
  • In transit: HTTPS/TLS 1.2+ everywhere. The Worker is served only over HTTPS and sets Strict-Transport-Security (app/entry.server.tsx:135-138) plus a restrictive Content-Security-Policy (app/entry.server.tsx:124-134).
  • Region: Cloudflare’s global network; D1 has a primary region chosen by Cloudflare at creation. The app does not pin a jurisdiction, so data may be processed outside the merchant’s country. Cloudflare’s standard contractual clauses / DPA apply.

4. Who can access it

  • The merchant, through the embedded Shopify admin. Every admin route is behind Shopify session-token authentication (authenticate.admin, configured in app/shopify.server.ts:39-69), so only staff with access to that Shopify store can read that store’s data.
  • The app operator — currently a single operator — via Cloudflare dashboard/wrangler access to the D1 database and Workers Logs, and via the /api/admin endpoint gated by the KDDAPP_ADMIN_KEY bearer secret. There is no separate staff tier and no per-record access log. Stated plainly: operator access is broad and audited only by Cloudflare’s own account audit log.
  • No other party, except the sub-processors in §6.

Isolation between merchants is enforced by shop-scoped queries in application code, not by separate databases. Every model carries a shop column and every read/write filters on it.

5. Retention

Summarised here; the authoritative, per-table version with the code paths is the data retention policy.

  • Session — deleted on app uninstall (DisableShopJob.disableShop, app/jobs/shop/DisableShopJob.ts:23-26) and again on shop/redact.
  • SurveyResponse — kept until the merchant deletes it, until a customers/redact webhook matches it, or until shop/redact. No automatic age-based expiry.
  • JobQueue — deleted when the job runs; dead-lettered rows are kept indefinitely for inspection (prisma/schema.prisma:58-60).
  • All other tables — kept for the life of the install, deleted on shop/redact.

6. Third parties (sub-processors)

PartyWhat it receivesWhen
Shopify Everything — it is the source and destination of order, customer and subscription data. Always.
Cloudflare Hosting (Workers), the database (D1), request/application logs (Workers Logs, enabled at wrangler.toml:27-28), and outbound operational alert email ([[send_email]], wrangler.toml). Always.
Klaviyo Only if the merchant’s shop domain is present in the KLAVIYO_KEYS secret. Receives a subscription lifecycle event containing the customer’s email address plus event properties (app/lib/integrations/klaviyo.server.ts:144-148). If the shop is not in the map, nothing is sent. Per lifecycle event, opt-in per shop.

No other outbound destination exists. There is no analytics vendor, no error tracking SaaS, and no CDN other than Shopify’s own for App Bridge/Polaris assets.

7. Deletion and data-subject requests

The app implements Shopify’s three mandatory compliance webhooks. All three are HMAC-verified by authenticate.webhook() before any work happens, so a forged request cannot trigger a deletion.

customers/redact and customers/data_request

app/routes/webhooks.customer_redact.ts — one handler serves both topics (declared together in shopify.app.omni-applications.toml). It deletes every SurveyResponse and EmailLog row for that shop whose customerId matches the payload’s customer id, or whose orderId matches an entry in orders_to_redact (webhooks.customer_redact.ts:45-60). Both the bare numeric id and the gid:// form are matched. The webhook payload is never logged — it is the personal data (webhooks.customer_redact.ts:21).

Honest limitation: the same delete-everything behaviour is applied to customers/data_request. A data request is a request to produce data, not to destroy it; the app currently produces nothing and deletes the matching survey and notification rows instead. Those two tables are the only buyer-keyed data the app holds, so nothing is withheld from a requester — but a merchant who receives a data request and expects the app to hand back the customer’s survey answers will get an empty response and no rows. See §8.

shop/redact

app/routes/webhooks.shop_redact.ts enqueues DeleteBillingScheduleJob (app/jobs/shop/DeleteBillingScheduleJob.ts), which deletes every shop-scoped row: SurveyResponse, EmailLog, Session, BillingSchedule, ContractActivity, DunningTracker, JobQueue, Survey, TrackingSettings, CarrierAlias, PostPurchaseFlow, CheckoutDiscountRule — twelve tables. Nothing the app holds for that shop survives.

Historical note, kept because it matters for anyone auditing an install made before 2026-07-25: until that date this job deleted BillingSchedule only, and every other shop-scoped row (including survey responses containing buyer-typed free text) survived a shop/redact. Stores uninstalled before that date may still have residue in D1 and should be purged by hand.

app/uninstalled

app/routes/webhooks.app.uninstalled.tsx enqueues DisableShopJob, which deletes the shop’s Session rows and deactivates its BillingSchedule (app/jobs/shop/DisableShopJob.ts:23-26). Configuration and survey data survive uninstall so that a reinstall within Shopify’s 48-hour window is not destructive; shop/redact then removes it.

Manual requests

A merchant or a data subject can email privacy@kddbrands.com. Requests are actioned manually against D1 by the operator. Target: 30 days, consistent with GDPR Article 12(3).

8. Known gaps — stated rather than papered over

  1. No field-level encryption. Session.accessToken and all other columns rely solely on Cloudflare’s at-rest encryption. Anyone with operator-level Cloudflare access can read every shop’s access token.
  2. Email addresses appear in application logs. The Klaviyo bridge logs the customer email at info level on both the success path (app/lib/integrations/klaviyo.server.ts:174) and the no-key path (:122-123). Those lines land in Cloudflare Workers Logs, which is therefore a secondary (short-lived) store of personal data. Webhook payloads are correctly not logged, and the no-profile-email path (:112-117) logs only event properties; these two lines are the exception and should be reduced to a hashed or truncated identifier.
  3. Shopify Admin API HTTP request logging is on (httpRequests: true, app/shopify.server.ts:56), so request URLs — which can carry ids — are logged.
  4. customers/data_request deletes rather than produces (§7).
  5. Single-operator model. No separation of duties, no per-record access audit trail, no formal on-call rota. See our security and incident commitments for what does exist.
  6. Tenant isolation is application-level, enforced by shop-scoped queries rather than by a database boundary. A missing where: {shop} clause in future code would be a cross-tenant leak; there is no database-level guard.

9. Children

The app is a merchant tool and is not directed at children. It stores no age data and makes no attempt to identify minors.

10. Changes

Material changes to this policy will be reflected here and, for changes that expand what is collected or who receives it, notified to installed merchants before taking effect.


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