Free Plan: Reply Cap and Monthly Reset
This document describes how the free plan reply limit is stored, enforced, and reset.
To move to the paid plan, merchants use Shopify App Pricing (hosted plan picker), not an in-app subscription charge — see Shopify App Pricing.
Database fields (shops table)
Section titled “Database fields (shops table)”| Column | Type | Description |
|---|---|---|
free_credit_limit | integer | Maximum number of AI replies allowed per month for this shop. Set when the store is created from .env FREE_PLAN_REPLIES_CAP (default 50). |
free_credits_used | integer | Number of AI replies used this month. Incremented by 1 each time the shop (on free plan) receives an AI reply. Reset automatically on UTC day 1 each month by the cron job (see below). |
When values are set or updated
Section titled “When values are set or updated”-
Store creation
When a new shop is created (OAuth install),free_credit_limitis set fromprocess.env.FREE_PLAN_REPLIES_CAP(default 50).free_credits_usedis set to 0.
Seeapp/shopify.server.js(shop create). -
Each AI reply (free tier path only)
After an AI reply is sent, the app incrementsfree_credits_usedby 1 only when the request used the free reply cap (not the USD wallet). If the shop is on the wallet path (plan === "paid"orcredit_balance > 0while plan is free),incrementFreeCreditsUsedis skipped.
Seeapp/lib/limits.server.js(incrementFreeCreditsUsed,shopUsesCreditWallet) andapp/lib/rag.server.js. -
Monthly reset (automatic cron)
On UTC calendar day 1 each month a scheduled job GETs the reset endpoint. The route resetsfree_credits_usedto0for allplan = 'free'shops in a singleupdateMany. Paid shops are not affected.Production URL (copy for your scheduler):
https://ai-chat.appifire.com/api/cron/reset-free-plan-repliesUse your real app origin if it differs (
SHOPIFY_APP_URL); path is always/api/cron/reset-free-plan-replies.- Cron expression (UTC):
10 0 1 * *(00:10 UTC, 1st of month). - No auth header needed. The route has a built-in UTC day-1 guard: if called on any day other than the 1st it returns
{ skipped: true }and does not write to the database — accidental calls on other days are completely safe. - Emergency manual reset: call
resetFreeCreditsUsedForFreePlanShops(prisma)fromapp/lib/limits.server.jsor hit the URL above on UTC day 1.
Ops reference (cron + compliance + analytics tables): Scheduled jobs, privacy & shop analytics.
- Cron expression (UTC):
Enforcing the limit (upgrade required)
Section titled “Enforcing the limit (upgrade required)”- Before generating an AI reply, the app checks the free plan limit in
checkReplyLimit(inapp/lib/limits.server.js). - If
free_credits_used >= free_credit_limit, the app throws REPLY_LIMIT_EXCEEDED and does not generate a reply. The API returns a 429 with a message that the monthly reply limit is exceeded and the merchant should upgrade to the paid plan. - The Billing page shows “X of Y replies used” for free plan using
free_credits_usedandfree_credit_limitso merchants can see usage and upgrade when needed.
Environment variable
Section titled “Environment variable”- FREE_PLAN_REPLIES_CAP
Used only when a new shop is created. Its value is stored inshops.free_credit_limitfor that shop. Default if unset: 50.
Existing shops keep their currentfree_credit_limit; change this env only affects new installs.
Migration
Section titled “Migration”- Migration
20260310000000_add_free_credit_limit_and_usedaddsfree_credit_limit(default 50) andfree_credits_used(default 0) toshops.
Existing shops get these defaults; new shops getfree_credit_limitfrom env at create time.
Free plan with a leftover USD wallet (after cancel)
Section titled “Free plan with a leftover USD wallet (after cancel)”If shops.plan is free but credit_balance > 0 (merchant canceled subscription but had not spent all purchased/included dollars), chat uses the credit wallet path, not the free monthly reply cap, until the balance reaches zero:
shopUsesCreditWalletinapp/lib/limits.server.jsis true whenplan === "paid"ORcredit_balance > 0.checkReplyLimitskips the 50-reply cap when the wallet path applies.deductCreditsForReplyreducescredit_balanceeven whileplan === "free".- After balance hits 0, behavior returns to the normal free reply cap (
free_credits_used/free_credit_limit).
Merchants cannot buy top-up packs without an active Shopify subscription (POST /app/billing/buy-credits checks activeSubscriptions). Full detail: Credits wallet: one-time & subscription.