Security and data processing

The least data that still answers the question.

Checkoutlab reads order webhooks but stores no customer fields: no name, email, address, IP or customer ID. Everything below describes what it does hold, where that sits, who can reach it and when it is deleted, in enough detail to put in front of your own privacy review.

Ask a security question
The summary

Three things worth knowing first.

No customer fields stored

The app requests no scope that would give it a customer name, email address or address, and reads none of the customer fields on the payloads it does receive. There is no column for them anywhere.

Tokens encrypted at rest

Your Shopify access and refresh tokens are encrypted with AES-256-GCM before they are written, with a key held outside the database. A dump on its own is not enough to act as the app on your store.

Deletion is automatic

Measurements are purged after your retention window, 400 days by default and yours to change. A button in Settings erases everything on the spot.

The detail

What is stored, exactly

Seven tables, and this is all of them:

  • Sessions. One row per installed shop: the shop domain, the Shopify access and refresh tokens, the granted scopes and expiry dates. The tokens are encrypted; the shop domain, the scopes and the dates are not, because every lookup filters on them and encrypting a column you have to search is a trade with no winner.
  • Experiments and Rules. Your configurations: key, name, type, status, split, target markets, conditions and the payment methods involved. No personal data.
  • Exposures. One row per experiment per checkout: a checkout token, the arm, the market handle, the device class, the order value and currency, and when it happened.
  • Conversions. The same, with an order ID, for checkouts that became orders.
  • Payment methods seen. The names of the payment methods that actually came through your checkout, with a count, so the experiment form can offer real choices instead of a free text field. No personal data.
  • Shop settings. Your retention window, when the last purge ran, and when your trial started.

Why the two identifiers exist. A checkout token and an order ID contain no identifying field, but they do point at a person indirectly, so we treat those two tables as personal data. They are there to deduplicate Shopify's at-least-once webhook delivery, and to make an erasure request actionable: without them there would be no way to find a given person's rows, and “we cannot find it” is not an answer to a deletion request.

Where it lives

  • Application: DigitalOcean App Platform (Amsterdam, the Netherlands).
  • Database: Supabase Postgres (AWS eu-central-1, Frankfurt, Germany).

All processing of app data happens inside the European Union, so no standard contractual clauses or other transfer mechanism is needed for it. The full sub-processor list, including the one third party involved on this marketing website, is in the privacy policy.

Encryption at rest, and why on top of the database

Database-side access control protects nothing once the database itself is in someone else's hands: a leaked connection string, a stolen backup, a compromised service key. So the tokens are encrypted before they are written, with a key that lives in the application environment and never in the database.

The algorithm is AES-256-GCM, which authenticates as well as encrypts: a modified ciphertext fails to decrypt instead of returning plausible rubbish. Keys can be rotated without downtime: the old and the new key both keep decrypting while everything newly written uses the new one.

There is deliberately no fallback to plaintext. If the key is missing, the app refuses to store a session rather than quietly writing a token in the clear, because that is the one failure mode that looks like everything is fine.

Who can reach it

  • Row level security is on for every table, with zero policies, and the database's anon and authenticated roles hold no grants. Prisma connects as the table owner and bypasses RLS, so the app is unaffected; nothing reaches these tables over Supabase's PostgREST endpoint. Supabase's own advisor reports rls_enabled_no_policy at INFO level for every table, which is the intended end state here rather than a finding. The protection lives in a version-controlled migration rather than a dashboard toggle, so a fresh environment comes up locked down instead of silently open.
  • Access is limited to the people who run the service, which at our size is a short list, each bound by confidentiality. It is used for operating and supporting the app, never to browse a shop's data out of interest.
  • Your shop's data is filtered by shop on every query. One shop cannot see another's experiments or results.
  • Transport is TLS everywhere, including the database connection, which is configured to require it rather than to prefer it, a default that silently falls back to plaintext.

What runs during a shopper's checkout

Nothing of ours. The variant is a Shopify payment customization function, which Shopify executes on its own infrastructure as part of the checkout. There is no request to our servers while someone is paying, which means our availability cannot affect your checkout and our logs contain nothing from it.

On the storefront, the theme app embed runs in the visitor's browser to pick an arm. It sends nothing to us, and there is no pixel, no fingerprinting and no third-party tag. A visitor ID in local storage counts as a cookie under the GDPR, so the embed writes one only when Shopify's Customer Privacy API says analytics processing is allowed. Without consent it uses the cart token, which already exists as a functional necessity and is stored nowhere. See what happens in your customers' browsers.

Retention and deletion

  • Measurements: 400 days by default. Settings lets you set the window anywhere from 30 to 1095 days. The rows have no value once an experiment is decided, which is the whole argument for a limit.
  • The purge is triggered by incoming webhooks, at most once a day per shop. A cron would be tidier but is another moving part that can silently stop; a webhook-driven purge cannot drift out of step with a shop that is actually in use, and a shop that is not in use has had its data erased on uninstall anyway.
  • Uninstalling: your session is deleted immediately, and Shopify sends a shop erasure request 48 hours later, which removes everything: experiments, rules, measurements, payment method names and settings.
  • Or erase it now. Settings has a Delete all data button that runs the same erasure, so you do not have to uninstall and wait to exercise that right.

The three mandatory privacy webhooks

Shopify sends every public app three privacy requests and expects a verified, in-time answer. All three are implemented, and the HMAC is verified before any of the work runs:

  • Customer data request. We hold no personal data about a shopper, so there is nothing to hand over, and that is the answer. The request is logged so there is a record that it arrived and what was answered; you answer the data subject yourself, which is what Shopify's flow expects for an app with no merchant-facing export.
  • Customer erasure. We delete the order rows for the orders named in the request, and the matching checkout rows with them, because a checkout token is of no use once its order is gone and leaving it would leave a pseudonymous trace. The aggregate counts shift slightly as a result, which is correct: an erasure that leaves the number behind is not an erasure.
  • Shop erasure. Everything belonging to the shop, sessions included. It is the same code the Delete all data button runs, so the two cannot disagree about what “everything” means.

Why the scopes are what they are

  • read_orders covers both the orders/create and checkouts/create webhooks. Checkouts fall under Shopify's abandoned-checkout model and have no scope of their own, so there is no narrower option. This scope is protected customer data under Shopify's rules; we request the minimum access level and read the payloads for the cart attributes, the totals and the payment method names only.
  • read_markets is for the market picker when configuring an experiment.
  • read_payment_customizations and write_payment_customizations let the app create its own function and switch it on when something is live and off when nothing is.

That is the whole list. No customer scope, no product scope, no theme scope, no storefront access token and no web pixel scope. If a future feature needed one, Shopify would ask you to approve the change before it took effect.

Reporting a vulnerability

Mail hello@root-agency.nl with enough detail to reproduce it. We will acknowledge within one business day and keep you updated until it is fixed. Please do not test against a live merchant's store, do not access data that is not yours, and give us a reasonable window before publishing. We do not run a paid bounty programme, but we will credit you if you want that.

What we do not claim

We are a small operation and we would rather be precise than impressive. We hold no SOC 2 report and no ISO 27001 certificate, and we do not claim either. What we do claim is on this page, and every item on it is implemented in the app's source rather than aspirational. If your procurement process needs something we cannot honestly tick, tell us and we will say so plainly.