Shopify Integration
Catalog rules, sync, checkout actions, and write safety.
Client
convex/shopify/shopifyClient.ts wraps Admin and Storefront GraphQL over fetchWithRetry — 3 attempts, retryable statuses 504, honors Retry-After (capped 10 s), exponential backoff. Warns on API-version mismatch vs SHOPIFY_EXPECTED_API_VERSION.
Catalog rules — the publication is the truth
The projectR publication (SHOPIFY_PROJECTR_PUBLICATION_ID) is the sole source of truth for what the app sells (catalogRules.ts):
- Eligibility:
status === "active"AND not taggedquiz-placeholder. - Availability: variant sellable if
inventoryPolicy CONTINUEor qty > 0; product sellable if active with ≥1 sellable variant. - Never compute
availableForSaleinline — every writer calls the shared rule.reconcileProductVariantsprunes variants Shopify no longer reports (cap 250/product).
Sync (productActions.ts): nightly cron at 03:17 MYT + the dashboard Sync button. Taking a product out of the app = unpublish from the channel (the webhook archives it) — no tags, no dashboard flags.
Customer sync
Inbound: the customers webhook matches through findProfileForBuyer, by shopifyCustomerId, then an email or phone the user verified in Better Auth, and fills only missing fields. It saves a Shopify customer id onto a profile only when the user verified that customer's email or phone. Outbound: signup creates a shopifySyncJobs row and a durable workflow calls the Admin API; status is queryable per profile.
Checkout and discounts
prepareCheckout— see Checkout Flow.validateDiscountuses the Admin APIcodeDiscountNodeByCodeexact match (the old search query fuzzy-matched) and returns one opaque rejection message — deliberately, to kill the code-enumeration oracle.- Checkout no longer creates a points-redemption discount. Points were removed on 2026-10-16, and
prepareCheckoutstill acceptsloyaltyPointsToRedeemonly so shipped app builds keep working.
Write safety
- The
SHOPIFY_WRITES_ALLOWEDgate (convex/helpers/shopifyWrite.ts).adminRequestWithMetadatathrowsSHOPIFY_WRITES_DISABLEDbefore any Admin API mutation leaves a deployment that does not set the variable to1. Mutations that would only queue a write callshopifyWritesAllowed()and refuse or skip. The variable is read per call, so setting or removing it needs no redeploy. Storefront cart calls are not gated. gid://prefix checks before every discount mutation (deleteShopifyDiscountskips non-GID ids).- Publication filtering at the webhook HTTP layer (
isKnownProduct/isKnownInventoryItem). shopifySyncPoolmaxParallelism 3 (Shopify concurrency limits).- The one-shared-live-store hazard (see Environments). Treat every Shopify write from a deployment with the flag set as production-impacting.
Webhook registration
webhookAdmin.ts holds the required topics and their route paths. Order matters: deploy the backend first, then register topics — registrations pointing at an undeployed route get dropped by Shopify after repeated 404s. The dashboard's Webhooks page drives registration via bulkCreateWebhooks and mirrors state into webhookSubscriptions.