Option A — Uninstall Users: Record & Goodbye Email
This document is the plan for handling app uninstalls: recording uninstalled shops in a dedicated table and sending them a goodbye email via Mailjet SMTP. 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).
1. Objective
Section titled “1. Objective”- 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 Mailjet SMTP, with a template that says sorry to see them go and asks for feedback (why they left, how to improve).
2. Shopify event: app/uninstalled
Section titled “2. Shopify event: app/uninstalled”- 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.jsxalready verifies the webhook, deletes sessions for that shop, and setsShop.status = "uninstalled". The shop row is kept for history. - New behaviour: Before or after marking the shop uninstalled, (1) copy relevant data into a new shops_uninstalled table, and (2) send the goodbye email via Mailjet SMTP (non-blocking so the webhook returns 200 quickly).
3. New database table: uninstalled shops
Section titled “3. New database table: uninstalled shops”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:
| Column | Type | Purpose |
|---|---|---|
id | UUID, PK | Unique row id. |
shop_domain | String, unique | e.g. store.myshopify.com. |
shop_name | String, optional | From Shop.shopName at uninstall time. |
shop_email | String, optional | Recipient for goodbye email (from Shop.shopEmail). |
shop_phone | String, optional | From Shop.shopPhone (typically entered manually in Settings; not provided by Admin API 2025-10). |
plan | String, optional | e.g. free / paid at uninstall. |
uninstalled_at | DateTime | When the webhook was processed. |
goodbye_email_sent_at | DateTime, optional | When the Mailjet email was sent (null if send failed or skipped). |
goodbye_email_error | String, optional | Store 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, updategoodbye_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")}4. Mailjet SMTP relay
Section titled “4. Mailjet SMTP relay”- Option A (recommended for “SMTP relay”): Use Mailjet’s SMTP with credentials from the Mailjet dashboard (SMTP server, port, username = API key, password = Secret key). Send from your backend with a generic Node SMTP client (e.g. nodemailer).
- Option B: Use Mailjet’s HTTP API (transactional email) if you prefer; the plan below assumes SMTP for simplicity.
Environment variables (e.g. in .env):
MAILJET_SMTP_HOST— e.g.in-v3.mailjet.comMAILJET_SMTP_PORT— e.g.587(TLS)MAILJET_API_KEY— SMTP usernameMAILJET_SECRET_KEY— SMTP passwordMAILJET_FROM_EMAIL— sender address (e.g.noreply@yourdomain.com)MAILJET_FROM_NAME— e.g.AppiFire AI Chat
Flow:
- In the uninstall webhook handler, after persisting the uninstalled-shop row, enqueue or call a small “send goodbye email” function.
- That function loads the row (or receives
shopDomain,shopEmail, etc.), builds the email (see template below), and sends via Mailjet SMTP. - If send succeeds, set
goodbye_email_sent_at(and cleargoodbye_email_error). If it fails, setgoodbye_email_errorand leavegoodbye_email_sent_atnull so you can retry later if desired. - 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 SMTP.
5. Goodbye email template
Section titled “5. Goodbye email template”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”.
6. Implementation checklist
Section titled “6. Implementation checklist”- Add
ShopsUninstalledmodel to Prisma schema and run migration. - In
webhooks.app.uninstalled.jsx:- Before or after marking the shop uninstalled, fetch the shop row (shopDomain, shopName, shopEmail, shopPhone, plan).
- Upsert into
ShopsUninstalled(byshop_domain): set shop snapshot,uninstalled_at, leavegoodbye_email_sent_at/goodbye_email_errornull initially. - Keep existing behaviour: delete sessions, set
Shop.status = "uninstalled".
- Add Mailjet SMTP env vars to
.envand.env.sample. - Implement a sendGoodbyeEmail (or similar) in
app/lib/email.server.js(or a dedicatedapp/lib/mailjet.server.js):- Accept
{ shopDomain, shopEmail, shopName }(and optionallyplan). - If no
shopEmail, return without sending and return a reason (e.g. “No email”). - Use nodemailer + Mailjet SMTP to send the goodbye template.
- Return success/failure so the webhook can set
goodbye_email_sent_atorgoodbye_email_error.
- Accept
- In the webhook: after upserting
ShopsUninstalled, call send good bye email (fire-and-forget or small async), then on completion update the row withgoodbye_email_sent_atorgoodbye_email_error. Ensure webhook returns 200 without waiting for SMTP. - Add the goodbye email template (subject + body) as constants or a small template function; support plain text and optionally HTML.
- QA: Uninstall the app on a test store, confirm one row in
shops_uninstalledand that the merchant receives the goodbye email. Re-run webhook (e.g. resend from Partner Dashboard) and confirm no duplicate row and idempotent behaviour.
7. File summary
Section titled “7. File summary”| Item | Action |
|---|---|
prisma/schema.prisma | Add ShopsUninstalled model. |
prisma/migrations/ | New migration for shops_uninstalled table. |
app/routes/webhooks.app.uninstalled.jsx | Fetch shop, upsert ShopsUninstalled, trigger goodbye email, keep existing session delete + status update. |
app/lib/email.server.js or app/lib/mailjet.server.js | Send goodbye email via Mailjet SMTP (nodemailer); template for “sorry to see you go” + feedback ask. |
.env / .env.sample | Document MAILJET_SMTP_HOST, MAILJET_SMTP_PORT, MAILJET_API_KEY, MAILJET_SECRET_KEY, MAILJET_FROM_EMAIL, MAILJET_FROM_NAME. |
8. Summary
Section titled “8. Summary”| Topic | Plan |
|---|---|
| Event | Shopify app/uninstalled webhook (already registered). |
| New table | shops_uninstalled: shop domain, name, email, phone, plan, uninstalled_at, goodbye_email_sent_at, goodbye_email_error. |
Mailjet SMTP (nodemailer); send once per uninstall to shop_email; template: “Sorry to see you go”, ask why they left and how to improve, reinstall CTA. | |
| Webhook | Verify HMAC, upsert uninstalled row, mark shop uninstalled, send email asynchronously, return 200. |
Next: implement the migration, webhook changes, and Mailjet send + template; then test with an uninstall on a development store.