Skip to content

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.


WhatTriggerUpdates
Free-plan monthly resetYour scheduler GETs one URL monthly (UTC)shops.free_credits_used → 0 where plan = 'free'
shops_installed_statsNo 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 webhookUpsert by domain; goodbye email fields; shopify_shop_id on uninstall
shops_uninstalled (rich snapshot)shop/redact via POST /webhooks/compliancearchiveShopSnapshotForRedact fills snapshot columns, then Shop is deleted (row in shops_uninstalled is kept / updated)
GDPR complianceShopify POST /webhooks/compliancecustomers/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.

ItemValue
HTTP methodGET
AuthNone (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
Implementationapp/routes/api.cron.reset-free-plan-replies.jsxresetFreeCreditsUsedForFreePlanShops 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.


Shopify delivers these to your app on POST /webhooks/compliance (configured in shopify.app.toml as compliance_topics).

TopicHandler behaviour (summary)
customers/data_requestLogged / support trail (payload varies).
customers/redactDeletes customer-linked chat data for the email in the payload when the shop still exists.
shop/redactRuns 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 model ShopsInstalledStats). One row per shop_id, FK cascade when the shop is deleted.
  • When: After a successful shop upsert during OAuth, app/shopify.server.js calls upsertShopsInstalledStats in a non-blocking way (errors are logged, install is not blocked).
  • Throttle: app/lib/shops-installed-stats.server.js skips refresh if the row’s computed_at is newer than ~4 hours (MIN_REFRESH_INTERVAL_MS), so repeat admin opens in the same day do not recompute every time.
  • What is computed: computeShopAnalyticsSnapshot in app/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.) that shop/redact writes onto shops_uninstalled.

Field list: see model ShopsInstalledStats in prisma/schema.prisma (mirrors the snapshot side of ShopsUninstalled where applicable).


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.

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.


  • External scheduler: GET the free-reset URL on UTC day 1 (see §1).
  • npx prisma migrate deploy in each environment (tables/columns for analytics + compliance).
  • Deploy app + shopify app deploy (or equivalent) so Partner Dashboard matches shopify.app.toml compliance URIs.
  • Monitor the GET endpoint for non-2xx on the 1st; mid-month accidental GETs should return 200 with skipped: true.