Skip to content

Uninstall: Record & Goodbye Email

This document is the implementation reference for app uninstall handling: recording uninstalled shops in a dedicated table and sending them a goodbye email via Brevo API. Shopify triggers an app/uninstalled webhook when a merchant uninstalls the app.

Prerequisites: Existing app/uninstalled webhook handler (webhooks.app.uninstalled.jsx) that marks the shop as status: "uninstalled". Phase 10 (register users) gives you shopName, shopEmail, shopPhone on Shop for the email recipient. Note: shopName and shopEmail are filled from Shopify on install and in Settings; shopPhone is manual entry in Settings (Admin API 2025-10 does not expose store address/phone).


  • Record every uninstall in a dedicated database table (for analytics, win-back, and compliance).
  • Send a goodbye email to the merchant (using their shop contact email) via Brevo API, with a template that says sorry to see them go and asks for feedback (why they left, how to improve).
  • Done: uninstall webhook stores ShopsUninstalled snapshot and updates Shop.status.
  • Done: sendGoodbyeEmail() uses Brevo API and writes success/error fields.
  • Done: .env / .env.sample document Brevo variables.
  • Pending per environment: run npx prisma migrate deploy where not yet applied.

  • When: Shopify sends a webhook to your registered URL (e.g. POST /webhooks/app/uninstalled) when a merchant uninstalls the app.
  • Current behaviour: Your webhooks.app.uninstalled.jsx already verifies the webhook, deletes sessions for that shop, and sets Shop.status = "uninstalled". The shop row is kept for history.
  • Current behaviour: On uninstall, the app upserts a shops_uninstalled row and triggers a non-blocking goodbye email via Brevo API.

Create a table to store a snapshot of each uninstall (and whether the goodbye email was sent). This keeps an audit trail and avoids re-sending if the webhook retries.

Suggested schema:

ColumnTypePurpose
idUUID, PKUnique row id.
shop_domainString, uniquee.g. store.myshopify.com.
shop_nameString, optionalFrom Shop.shopName at uninstall time.
shop_emailString, optionalRecipient for goodbye email (from Shop.shopEmail).
shop_phoneString, optionalFrom Shop.shopPhone (typically entered manually in Settings; not provided by Admin API 2025-10).
planString, optionale.g. free / paid at uninstall.
uninstalled_atDateTimeWhen the webhook was processed.
goodbye_email_sent_atDateTime, optionalWhen the Brevo email was sent (null if send failed or skipped).
goodbye_email_errorString, optionalStore last error message if send failed (for debugging).
  • Unique on shop_domain: One row per shop; if the webhook fires again (e.g. retry), you can skip insert or update the same row (e.g. retry email send, update goodbye_email_sent_at).
  • No FK to shops: Keeps uninstall history even if you later purge or anonymise old shop rows.

Prisma sketch:

model ShopsUninstalled {
id String @id @default(uuid())
shopDomain String @unique @map("shop_domain")
shopName String? @map("shop_name")
shopEmail String? @map("shop_email")
shopPhone String? @map("shop_phone")
plan String? @map("plan")
uninstalledAt DateTime @default(now()) @map("uninstalled_at")
goodbyeEmailSentAt DateTime? @map("goodbye_email_sent_at")
goodbyeEmailError String? @map("goodbye_email_error") @db.Text
@@map("shops_uninstalled")
}

  • Use Brevo transactional API with API key authentication.
  • Send from backend using POST https://api.brevo.com/v3/smtp/email.

Environment variables (e.g. in .env):

  • BREVO_API_KEY — Brevo API key
  • BREVO_FROM_EMAIL — sender address (e.g. noreply@yourdomain.com)
  • BREVO_FROM_NAME — e.g. AppiFire AI Chat
  • BREVO_REPLY_TO_EMAIL — optional reply-to address
  • BREVO_REPLY_TO_NAME — optional reply-to name

Flow:

  1. In the uninstall webhook handler, after persisting the uninstalled-shop row, enqueue or call a small “send goodbye email” function.
  2. That function loads the row (or receives shopDomain, shopEmail, etc.), builds the email (see template below), and sends via Brevo API.
  3. If send succeeds, set goodbye_email_sent_at (and clear goodbye_email_error). If it fails, set goodbye_email_error and leave goodbye_email_sent_at null so you can retry later if desired.
  4. Important: The webhook must return 200 quickly. So either send the email asynchronously (e.g. fire-and-forget with .catch() and log errors) or from a background job; avoid blocking the webhook response on API call latency.

Subject (example):
We're sorry to see you go — quick feedback?

Body (plain text and/or HTML):

  • Short line: Sorry to see you go. We’re sorry that you’ve uninstalled AppiFire AI Chat.
  • Ask for feedback:
    • “If you have a moment, we’d love to know why you left and how we can make our product better.”
    • Optional: link to a simple feedback form (e.g. Typeform, Google Form) or a “reply to this email” CTA.
  • Reinstall:
    • “Changed your mind? You can reinstall the app from the Shopify App Store anytime.”
  • Sign-off:
    • e.g. “Thank you for trying us. — The AppiFire AI Chat team”

Personalisation: Use shopName (or shop domain) and send to shopEmail. If shopEmail is missing, skip sending and store a short goodbye_email_error like “No email on file”.


  • Add ShopsUninstalled model to Prisma schema and migration.
  • In webhooks.app.uninstalled.jsx:
    • Fetch shop row and upsert uninstall snapshot by shop_domain.
    • Keep existing behaviour: delete sessions and set Shop.status = "uninstalled".
    • Trigger goodbye email and persist send success/failure fields.
  • Add Brevo env vars to .env and .env.sample.
  • Implement sendGoodbyeEmail in app/lib/email.server.js.
  • Keep webhook non-blocking for email send failures.
  • Apply migration in every deployment environment: npx prisma migrate deploy.
  • QA uninstall on each target environment/store after deployment.

7. GDPR shop/redact, shops_uninstalled archive & shops_installed_stats

Section titled “7. GDPR shop/redact, shops_uninstalled archive & shops_installed_stats”

Shopify sends shop/redact ~48 hours after uninstall (mandatory compliance). It is not the same event as app/uninstalled.

ConcernWhere it lives
Compliance handlerapp/routes/webhooks.compliance.jsx — topics customers/data_request, customers/redact, shop/redact on POST /webhooks/compliance (see shopify.app.toml compliance_topics).
Before deleting the Shop rowarchiveShopSnapshotForRedact in app/lib/shops-uninstalled-archive.server.js upserts shops_uninstalled with the same rich metric bundle used for live analytics (tenure days, distinct chat days, catalog counts, OpenRouter lifetime totals, subscription snapshot, churn_metrics_json, etc.).
Rolling stats while installedTable shops_installed_stats (one row per shop, FK cascade on shop delete). Refreshed from afterAuth via upsertShopsInstalledStats in app/lib/shops-installed-stats.server.js (throttled ~4 hours, non-blocking). Metrics are built by computeShopAnalyticsSnapshot in app/lib/shop-analytics-snapshot.server.js so installed + redact snapshots stay aligned. There is no HTTP cron for this table — only merchant OAuth revisits (and the throttle window).

Ops / URLs: Scheduled jobs, privacy & shop analytics — includes the single external cron URL (free-plan free_credits_used reset only) vs webhook-driven analytics.

The early sections of this doc describe the original shops_uninstalled columns for goodbye email; production schema adds many optional snapshot columns filled on shop/redact (and shopify_shop_id is also written on uninstall upsert).


ItemAction
prisma/schema.prismaAdd ShopsUninstalled model.
prisma/migrations/New migration for shops_uninstalled table.
app/routes/webhooks.app.uninstalled.jsxFetch shop, upsert ShopsUninstalled, trigger goodbye email, keep existing session delete + status update.
app/lib/email.server.js or app/lib/brevo.server.jsSend goodbye email via Brevo API; template for “sorry to see you go” + feedback ask.
.env / .env.sampleDocument BREVO_API_KEY, BREVO_FROM_EMAIL, BREVO_FROM_NAME (+ optional reply-to vars).

TopicStatus
EventShopify app/uninstalled webhook (already registered).
New tableshops_uninstalled implemented: snapshot + goodbye send tracking fields.
EmailBrevo API implemented; send once per uninstall to shop_email; logs errors without blocking webhook.
WebhookImplemented: verify HMAC, upsert uninstall row, mark shop uninstalled, fire async email update path.

Next: ensure npx prisma migrate deploy has run in every environment, then run uninstall QA in staging/production stores.