ProjectR Docs
Backend (Convex)

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 tagged quiz-placeholder.
  • Availability: variant sellable if inventoryPolicy CONTINUE or qty > 0; product sellable if active with ≥1 sellable variant.
  • Never compute availableForSale inline — every writer calls the shared rule. reconcileProductVariants prunes 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.
  • validateDiscount uses the Admin API codeDiscountNodeByCode exact 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 prepareCheckout still accepts loyaltyPointsToRedeem only so shipped app builds keep working.

Write safety

  1. The SHOPIFY_WRITES_ALLOWED gate (convex/helpers/shopifyWrite.ts). adminRequestWithMetadata throws SHOPIFY_WRITES_DISABLED before any Admin API mutation leaves a deployment that does not set the variable to 1. Mutations that would only queue a write call shopifyWritesAllowed() and refuse or skip. The variable is read per call, so setting or removing it needs no redeploy. Storefront cart calls are not gated.
  2. gid:// prefix checks before every discount mutation (deleteShopifyDiscount skips non-GID ids).
  3. Publication filtering at the webhook HTTP layer (isKnownProduct / isKnownInventoryItem).
  4. shopifySyncPool maxParallelism 3 (Shopify concurrency limits).
  5. 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.

On this page