TransparencyPassport

Security at TransparencyPassport

Last updated: 10 October 2026

This page explains how the app protects your data.

Found a security problem? Email [email protected] with "Security" in the subject. Please give us time to fix it before you tell others.

Less data, less risk

  • The app asks Shopify for one permission only: read_products. It cannot read customers, orders or payments.
  • Passport analytics are anonymous. They hold no IP address, user agent, cookie or customer ID.
  • Access links count how often they are used, not who uses them.
  • Card and bank details never reach us. Shopify handles billing.

The database is server-only

  • Passport and app data live in Google Cloud Firestore. Only our server reaches it, through the Firebase Admin SDK with a service account.
  • The Firestore security rules deny every browser and client app. Nobody can read or write the database from a web page.
  • Shopify sign-in sessions (shop domain, access token, scopes) live in a Supabase PostgreSQL database (Ireland, EU) that only our server can reach, over TLS with a private connection string. Row-level access from browsers is not enabled; the database has no public API keys in use.
  • Google Cloud, Supabase and DigitalOcean encrypt stored data by default. Traffic to the app uses HTTPS.

Every request is checked

  • Admin pages and admin API calls are authenticated with Shopify session tokens. Every read and write is scoped to the shop in that verified session. A shop cannot reach another shop's data.
  • Webhooks (uninstall, plan changes, product changes, privacy requests) are accepted only with a valid Shopify HMAC signature. Invalid ones get 401.
  • The storefront blocks load through Shopify's app proxy. The app checks Shopify's signature and takes the shop from the signed request, never from the page.
  • The scheduled job route needs a secret bearer token, compared in constant time. Without the secret set, the route is off.
  • Input is validated on the server: shop domains must be *.myshopify.com, IDs must match a safe pattern, settings and CSV rows are checked field by field, request bodies have size limits. Publishing checks identifiers strictly (GTIN and GLN check digits, EORI format, lot and serial characters).

Plans are enforced on the server

  • Your plan comes from Shopify's Billing API (billing.check), checked on the server and cached for one minute. A copy is written by the server only.
  • Limits (passports, steps, AI generations) and plan features (documents, access links, translations, backups) are checked on the server for every request that uses them. Changing the page in the browser cannot get around them.

Public pages show public data only

  • The passport page, the product-page blocks, the QR-code resolver and the machine-readable passport (/dpp/<shop>/<handle>.json) read only the published copy. Drafts never appear.
  • A role filter decides which fields each audience sees. Fields reserved for repairers, recyclers or authorities (such as manufacturer contact details, EORI, batch data and facility numbers) are removed on the server before the page or JSON is built. Tests check that restricted fields never leak into the public view.
  • Public pages receive only display settings. The notification email and internal fields (storage keys, flags) are left out.
  • When you unpublish, delete a product, or uninstall, the live copy is removed, and the CDN cache is purged when the CDN is in use.
  • An access link carries a signed token: the shop, product, role, expiry date and link ID, signed with HMAC-SHA256 and a server secret. A changed token fails the check.
  • A token works only if the signature matches, it has not expired, the link exists and is not revoked, the shop's plan includes access links, and it is used for its own passport.
  • Links expire after 90 days by default (up to 3 years). You can revoke a link at any time; it stops working at once.
  • Role views (page, JSON and documents) are never stored in the CDN cache.

Compliance documents

  • Files go straight from your browser to the storage bucket through a signed upload address that names the exact file, type and size, and expires after 15 minutes.
  • The server then reads the file back and checks the size (20 MB at most), the real file type (PDF, PNG, JPG or WEBP, by its first bytes) and the SHA-256 fingerprint your browser computed. Anything that does not match is deleted.
  • If a virus scanner is set up (ClamAV), every file is scanned before it is accepted. If the scanner is down, uploads are refused, not let through.
  • Publishing locks the documents of that version: they cannot be changed or deleted, so a passport always shows the files it showed when published.
  • Downloads check the audience: public files for anyone, other files only with an access link whose role covers them. Files are served through signed storage links that expire after 5 minutes, with nosniff and a sandbox policy when the app serves them itself.
  • The page shows each file's SHA-256 checksum ("Checksum recorded <date>") and lets anyone compare a download with it ("This file matches the checksum recorded on <date>"). That proves the file is unchanged since upload, not that its content is true. The app never labels a file "verified".

Backups

  • Every night, each shop on the Business plan gets a backup: one gzipped bundle of its passports, versions, live copies, settings, templates, access links and registry records, plus a copy of every document file.
  • Each bundle carries a SHA-256 checksum and an HMAC signature with a separate backup secret. The app reads every bundle back after writing it, and refuses to restore a bundle whose checksum or signature does not match, or that belongs to another shop.
  • Backups go to a second storage bucket with a different provider from the main one, so one provider's outage or account problem does not take both copies.
  • Bundles are kept for 35 days by default, then deleted. Document copies are stored once per fingerprint and expire under the backup bucket's own expiry rule.
  • Restores start with a dry run that shows what would change. A tested script restores a shop from a bundle alone, even if the database is lost.
  • The bundle is plain JSON, so an independent DPP service provider could hold a copy once the EU sets up that scheme.

Shops on other plans can export all passports and analytics as CSV at any time.

Abuse limits

RouteLimit
Analytics events (/api/track)60 per IP address per minute; only for published passports; 4 KB body limit
QR-code resolver (/01/<GTIN>…)120 per IP address per minute
Document downloads120 per IP address per minute
Machine-readable passport (/dpp/…)240 per IP address per minute
Admin changes (saves, publishes, billing, bulk, AI)300 per shop per minute

Counters are shared across servers through Redis, expire after a minute, and are never written to the database. Publishing and background jobs use locks, so two changes to the same shop cannot overwrite each other.

Background jobs are safe to retry

Bulk jobs record a done marker for every product and the job ID on every passport they change. If a server stops mid-job, another one resumes it without applying a change twice. Duplicate requests with the same idempotency key start only one job.

Secrets

API keys, the database URL, the Firebase service account, the storage keys, the access-link secret, the backup signing secret and the job secret are stored as encrypted secrets in our hosting platform. They are not in the code repository. In production the app refuses to issue access links or backups if their secret is missing.

Shopify access tokens are stored in the session records in Supabase PostgreSQL and deleted when you uninstall.

Outside services

  • Email goes through Resend only when it is switched on. Messages carry product names, links, counts and document dates; every value is escaped in the HTML.
  • Shopify Flow triggers go to your own store through Shopify's Admin API.
  • EU registry: the app does not connect to the EU yet. It only records registration requests.

Monitoring

  • The app runs behind a health check that tests Firestore, PostgreSQL and Redis (when configured); a failed deploy is rolled back automatically.
  • Error reports can go to Sentry. It is set up not to send IP addresses or cookies.
  • Automated tests cover plan limits, webhook handling, privacy erasure (including files and backups), analytics validation, role views, documents, backups and restores, jobs and rate limits.

Erasure

  • On uninstall, the app deletes its Shopify session and takes every passport offline at once.
  • On Shopify's shop/redact request, 48 hours later, it erases all the shop's data: database records (including analytics and access links), document files and backup bundles, and any sessions left.
  • customers/data_request and customers/redact are acknowledged. There is no customer data to return or delete.