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/)
| Group | Modules | Responsibility |
|---|---|---|
| API surface | dashboard/ | Admin queries + mutations (analytics, voucher ops, overrides, groups) |
mobileLoyalty/ | Mobile API surface (stamps, redemptions) | |
| Loyalty | gamificationLoyalty/ | Stamps, shake-to-win, voucher redemption, order→stamps workflow and stamp outcomes |
| Commerce | shopify/, cartSync/, checkoutSessions/, orderHistory/ | Catalog mirror + sync, cart mirror, checkout lifecycle, orders |
| Vouchers | voucherTemplates/vouchers logic in gamificationLoyalty/ + dashboard/ | Templates, issuance, revocation, Shopify codes |
| Campaigns | duitRaya/, seasonalCampaign/, content/ | QR claim campaigns, bulk generation, home content |
| Engagement | productEngagement/, beauty/, search/, skinScan/, support/ | Reviews/moderation, quiz, search + analytics, skin scan, contact |
| Identity | auth/, auth.ts, userAddresses/ | Better Auth, OTP, linking, hard delete, address book |
| Infra | notification/, storage/, webhooks/, routes/, helpers/, migrations/, monitoring/, crons.ts | Notification pipeline (toMobileFormat() field firewall lives here), R2, ingestion, HTTP router, shared helpers, backfills |
The five cross-cutting rules
- Aggregate maintenance. Every
ctx.db.insert/patch/deleteon a tracked table must call the matching@convex-dev/aggregatemethod in the same transaction. Miss one and dashboard stats silently drift. See Schema. - Auth pattern. Queries use
safeGetAuthUser(returns null); mutations usegetAuthUser(throws). Response shape is{ success, message, data }. Functions that take auserIdargument or make expensive external calls must beinternal. - Notifications. All producers go through
internal.notification.mutations.enqueueNotification— never a direct push call. - Rewards basis is the subtotal (no shipping/tax). The workflow arg carrying it is named
orderTotal— a known trap, documented in Order to Rewards. - 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 anormalizeIdfallback — 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 withfor await. - Keep new public functions auth-derived; anything accepting a userId is
internalby default.