ProjectR Docs
Backend (Convex)

Backend (Convex)

The Convex backend: module map, schema groups, and cross-cutting rules.

packages/backend is the single source of truth for the schema, all business logic, and the API types both apps compile against. Convex 1.47.0, Better Auth 1.6.33, and a large set of Convex components (workflows, workpools, aggregates, rate limiters, sharded counters, R2, Resend, Expo push).

Module map (convex/)

GroupModulesResponsibility
API surfacedashboard/Admin queries + mutations (analytics, voucher ops, overrides, groups)
mobileLoyalty/Mobile API surface (stamps, redemptions)
LoyaltygamificationLoyalty/Stamps, shake-to-win, voucher redemption, order→stamps workflow and stamp outcomes
Commerceshopify/, cartSync/, checkoutSessions/, orderHistory/Catalog mirror + sync, cart mirror, checkout lifecycle, orders
VouchersvoucherTemplates/vouchers logic in gamificationLoyalty/ + dashboard/Templates, issuance, revocation, Shopify codes
CampaignsduitRaya/, seasonalCampaign/, content/QR claim campaigns, bulk generation, home content
EngagementproductEngagement/, beauty/, search/, skinScan/, support/Reviews/moderation, quiz, search + analytics, skin scan, contact
Identityauth/, auth.ts, userAddresses/Better Auth, OTP, linking, hard delete, address book
Infranotification/, storage/, webhooks/, routes/, helpers/, migrations/, monitoring/, crons.tsNotification pipeline (toMobileFormat() field firewall lives here), R2, ingestion, HTTP router, shared helpers, backfills

The five cross-cutting rules

  1. Aggregate maintenance. Every ctx.db.insert/patch/delete on a tracked table must call the matching @convex-dev/aggregate method in the same transaction. Miss one and dashboard stats silently drift. See Schema.
  2. Auth pattern. Queries use safeGetAuthUser (returns null); mutations use getAuthUser (throws). Response shape is { success, message, data }. Functions that take a userId argument or make expensive external calls must be internal.
  3. Notifications. All producers go through internal.notification.mutations.enqueueNotification — never a direct push call.
  4. Rewards basis is the subtotal (no shipping/tax). The workflow arg carrying it is named orderTotal — a known trap, documented in Order to Rewards.
  5. One live Shopify store for every environment. A deployment writes to it only when it sets SHOPIFY_WRITES_ALLOWED=1, and then every write lands in production Shopify. Rollback helpers: removeVoucherCodeFromShopify, testing/debug:purgeAvailablePoolRows.

Conventions

  • Timestamps are ms numbers; roles are lowercase.
  • Deep-link-reachable queries take v.string() ids with a normalizeId fallback — malformed ids return null, never throw (a throw becomes a client render crash + websocket reconnect loop).
  • The convex-lint hook blocks DB .filter() predicates with column comparators — use indexes, JS-filter an index-bounded set, or stream with for await.
  • Keep new public functions auth-derived; anything accepting a userId is internal by default.

On this page