Scheduled jobs, privacy & shop analytics
Which URLs need an external cron, how GDPR compliance webhooks work, and how shops_installed_stats vs shops_uninstalled are filled.
This page is the ops reference for anything that looks like a “cron” in the app: the only HTTP endpoint you must schedule yourself is the free-plan reply counter reset. Shop analytics tables are updated by OAuth and Shopify webhooks, not by a second cron job.
At a glance
Section titled “At a glance”| What | Trigger | Updates |
|---|---|---|
| Free-plan monthly reset | Your scheduler GETs one URL monthly (UTC) | shops.free_credits_used → 0 where plan = 'free' |
shops_installed_stats | No cron. Shopify OAuth afterAuth (throttled, fire-and-forget) | One row per shop; same metric family as the redact snapshot |
shops_uninstalled (basic row) | app/uninstalled webhook | Upsert by domain; goodbye email fields; shopify_shop_id on uninstall |
shops_uninstalled (rich snapshot) | shop/redact via POST /webhooks/compliance | archiveShopSnapshotForRedact fills snapshot columns, then Shop is deleted (row in shops_uninstalled is kept / updated) |
| GDPR compliance | Shopify POST /webhooks/compliance | customers/data_request, customers/redact, shop/redact — see shopify.app.toml compliance_topics |
1. Free-plan reply counter reset (external cron — only scheduled HTTP job)
Section titled “1. Free-plan reply counter reset (external cron — only scheduled HTTP job)”Purpose: Reset shops.free_credits_used to 0 for every shop with plan = 'free' on UTC calendar day 1 each month.
| Item | Value |
|---|---|
| HTTP method | GET |
| Auth | None (no bearer). Safety: the handler returns { skipped: true } on any day other than UTC day 1 and does not write to the database. |
| Route (path) | /api/cron/reset-free-plan-replies |
| Implementation | app/routes/api.cron.reset-free-plan-replies.jsx → resetFreeCreditsUsedForFreePlanShops in app/lib/limits.server.js |
| Suggested cron (UTC) | 10 0 1 * * — e.g. 00:10 UTC on the 1st |
Production URL (copy into your scheduler):
https://ai-chat.appifire.com/api/cron/reset-free-plan-replies
Replace the origin with your real app URL if different (SHOPIFY_APP_URL); the path is always /api/cron/reset-free-plan-replies.
Not covered by this job: shops_installed_stats, shops_uninstalled, billing, or OpenRouter usage — those have no monthly HTTP cron in this product.
Merchant-facing detail: Free Plan: Reply Cap and Monthly Reset.
2. GDPR mandatory compliance webhooks
Section titled “2. GDPR mandatory compliance webhooks”Shopify delivers these to your app on POST /webhooks/compliance (configured in shopify.app.toml as compliance_topics).
| Topic | Handler behaviour (summary) |
|---|---|
customers/data_request | Logged / support trail (payload varies). |
customers/redact | Deletes customer-linked chat data for the email in the payload when the shop still exists. |
shop/redact | Runs archiveShopSnapshotForRedact (app/lib/shops-uninstalled-archive.server.js) so shops_uninstalled holds the analytics snapshot, clears sessions, then deletes the Shop row (cascades remove shops_installed_stats and other tenant data). |
Code: app/routes/webhooks.compliance.jsx.
3. shops_installed_stats — how stats update (no cron URL)
Section titled “3. shops_installed_stats — how stats update (no cron URL)”- Table:
shops_installed_stats(Prisma modelShopsInstalledStats). One row pershop_id, FK cascade when the shop is deleted. - When: After a successful shop upsert during OAuth,
app/shopify.server.jscallsupsertShopsInstalledStatsin a non-blocking way (errors are logged, install is not blocked). - Throttle:
app/lib/shops-installed-stats.server.jsskips refresh if the row’scomputed_atis newer than ~4 hours (MIN_REFRESH_INTERVAL_MS), so repeat admin opens in the same day do not recompute every time. - What is computed:
computeShopAnalyticsSnapshotinapp/lib/shop-analytics-snapshot.server.js— the same snapshot shape (tenure, chat counts, catalog counts, OpenRouter totals, subscription snapshot,churn_metrics_json, widget flags, etc.) thatshop/redactwrites ontoshops_uninstalled.
Field list: see model ShopsInstalledStats in prisma/schema.prisma (mirrors the snapshot side of ShopsUninstalled where applicable).
4. shops_uninstalled — two phases
Section titled “4. shops_uninstalled — two phases”A. On app/uninstalled
Section titled “A. On app/uninstalled”app/routes/webhooks.app.uninstalled.jsx upserts shops_uninstalled by shop_domain with uninstall metadata used for the goodbye email path (and stores shopify_shop_id when known). It does not replace the full GDPR snapshot block by itself.
B. On shop/redact (~48h later)
Section titled “B. On shop/redact (~48h later)”If a Shop row still exists, archiveShopSnapshotForRedact merges the full analytics snapshot into shops_uninstalled (including shop_redacted_at, tenure / activity / catalog / chat / OpenRouter / subscription / widget / churn_metrics_json, etc.), then compliance handler deletes the Shop row.
Field list: model ShopsUninstalled in prisma/schema.prisma.
5. Deploy checklist (ops)
Section titled “5. Deploy checklist (ops)”- External scheduler: GET the free-reset URL on UTC day 1 (see §1).
-
npx prisma migrate deployin each environment (tables/columns for analytics + compliance). - Deploy app +
shopify app deploy(or equivalent) so Partner Dashboard matchesshopify.app.tomlcompliance URIs. - Monitor the GET endpoint for non-2xx on the 1st; mid-month accidental GETs should return 200 with
skipped: true.
Related docs
Section titled “Related docs”- Uninstall users & GDPR snapshot flow — §7 ties uninstall + redact + installed stats together.
- Free Plan: Reply Cap and Monthly Reset — merchant-oriented reset behaviour.
- Links — quick copy of the production cron URL.