Isolation and privacy you can check.

Each guarantee below names the mechanism that enforces it and how it is checked. For the legal terms, the privacy page is the source.

Run it: your agents run it; your team watchesIncludes parts not in production yet

Guarantees

In production

An API key or a dashboard session decides the workspace before any query runs. From there, Postgres itself fences each brand's rows, and every ClickHouse query carries the workspace.

GuaranteeMechanismHow it is checked
Tenant isolation in PostgresEvery table has row-level security forced on, with a per-tenant policy. The API connects as a role that cannot bypass it and owns no table.A CI guard migrates a fresh database and reads what it enforces from the catalog, not from the migration files: every table forced and tenant-scoped, and every SECURITY DEFINER function listed, with its search path pinned and pg_temp last.
Tenant scope in ClickHouseRealized order facts and events carry tenant_id, and every query filters on it. The query is the boundary.Every realized figure is read through one query builder that takes the tenant, and a test fails on any query or app file that reads the table another way. Event queries take the tenant the same way, by convention rather than under that test.
API keysStored only as SHA-256 hashes. The full key is shown once, when it is created. A public key, the one meant for page source, reaches only the routes built for a shopper's browser.A key from another workspace gets 404 for your offers, the same answer as an id that does not exist. A public key on a secret route gets 403.
Dashboard sessionsChecked against the session store, Cloudflare KV, on every request. A signed cookie is never trusted on its own.Sign out, then replay the copied cookie: it stops working within KV's propagation time, up to about 60 seconds.
Team rolesOwner, admin, member and read-only, enforced by the API on every dashboard call. Team and key changes need an admin. Only an owner grants or removes owner, and a workspace always keeps one.The API refuses a read-only member's write whatever the screen shows. Team and key routes answer 403 to any API key, even a secret one; they work only from the dashboard and are left out of the OpenAPI document.

Store grants

In production

A store's Shopify grant is stored encrypted and accepted only in its expiring form, and a delivery that cannot be trusted is checked with Shopify before it can disconnect the store.

  • Shopify access and refresh tokens are stored encrypted with AES-256-GCM. Only expiring offline grants are accepted; a non-expiring grant is refused.
  • A token within 5 minutes of expiry is refreshed when it is used, under a per-shop lock, and the new pair is saved with a compare-and-set, so two refreshes cannot overwrite each other.
  • Every webhook is HMAC-verified, also against the previous client secret while a secret is being rotated. A delivery whose payload names a different shop than its header is rejected.
  • Uninstall and scopes-update deliveries are deduplicated by webhook id and body digest. An uninstall that is late, replayed or older than the current grant is checked against Shopify before anything disconnects, and re-checked after 2, 10 and 60 minutes while the answer is unclear. A disconnect never overrides a newer reinstall.
  • A daily job disconnects a store only after Shopify has precisely refused its grant for at least 20 hours with no successful call in between, and never more than 5% of stores in one run (at least one). Above that it disconnects none and records the disconnects it held back.
  • A store connects only through Shopify's managed install. There is no install link and no OAuth callback, so nobody can connect a store by sending someone a link.

Two edge cases still disconnect a store without a confirmed refusal: when the re-check cannot be queued, and when the last re-check's answer is still unclear.

Buyer identity

In production

Before an order is saved, a buyer's name, email, phone number, street address, city, postal code, IP address, browser user agent and payment details are removed, with receipts and order notes. The removal runs at every depth of Shopify's payload: a refund's payment details and a shipping line's phone go too.

  • Shopify's customers/data_request, customers/redact and shop/redact webhooks are verified, queued and handled automatically.
  • A customer redaction unlinks that customer's orders from visitors and sessions and keeps the money. It still applies when it arrives before the order is stored.
  • shop/redact deletes an uninstalled store's orders, offers and events.
  • Browser events expire 13 months after they happen.
  • Every request is recorded with its report, which the brand sees on the dashboard's Privacy page or at GET /privacy/requests.
  • Kept on each order, so a brand can tell new buyers from returning ones and compare markets and devices: Shopify's customer id, the order's place in that customer's history, the checkout language, the shipping country and state or province, and the device class and browser family. A customer redaction clears the customer id. On the dev API, not in production yet

The privacy policy is the legal text: what is collected, why, for how long, where it is processed and how to ask for deletion.

Rate limits

In production

Limits are counted per bucket by a Durable Object, so they hold across Cloudflare's machines. A per-machine limiter in front of it only sheds floods.

Secret key300 requests a minute per key.
Public key120 a minute per key and connecting IP, so one busy visitor does not throttle the whole store.
EventsPOST /events adds 60 a minute per key and visitor id.
Server-side renderingA server holding a secret key can name the shopper in Offer-Suite-Buyer-IP, and each shopper then gets a bucket of 120 a minute. The header counts only on public routes and is ignored from a public key.

The buyer's IP is used to count the limit and is forwarded to Shopify as Shopify-Storefront-Buyer-IP when a cart is created. It is never stored. If the counter cannot answer, the call goes through, so a limiter fault never blocks a storefront. These are the limits the buy box runs under.

What it does not do yet

  • ClickHouse has no database row-level security. Isolation there is the tenant_id filter in every query.
  • A key is secret or public, with no narrower permissions and no expiry date. It works until an admin revokes it.
  • Staff sign in by an emailed link, which expires after 10 minutes, or straight from the Shopify admin. There is no single sign-on yet.
  • Each store gets its own workspace; a second store cannot join an existing one yet. Partly available

Questions

Can one brand's agent read another brand's data?

No. The key decides the workspace, every Postgres table enforces row-level security for it under a database role that cannot bypass it, and every ClickHouse query filters on the workspace. A key from another workspace gets 404, the same answer as an id that does not exist.

Does Offer Suite store my customers' names or email addresses?

No. Names, email addresses, phone numbers, street addresses, cities, postal codes, IP addresses, browser user agents and payment details are removed from every order before it is saved. The privacy policy lists what is kept.

What happens to a store's data after it uninstalls?

Shopify sends shop/redact after the uninstall, and Offer Suite then deletes that store's orders, offers and events. An uninstall that looks late or replayed is checked with Shopify first, so a stale delivery cannot disconnect a store that is still installed.