Mayowa

ayowa

E-Commerce SolutionQ1 2026

TagScrubs Loyalty Reward System

Built as a fully embedded Shopify app, this loyalty rewards system automatically tracks qualifying customer purchases via webhooks and awards 'notches' toward configurable reward thresholds. It covers the full reward lifecycle — automatic unlock, staff issuance, and customer redemption — with an admin dashboard, a storefront-proxied customer portal, receipt uploads for bonus notches, social follow bonuses, and fire-and-forget email notifications throughout.

Demo coming soon
Stable Production Release

Project Screenshots

01 — The Challenge

No Customisable Loyalty Layer on Shopify

The client needed a loyalty program built around specific business rules: awarding points only on items priced at or above a threshold, using a custom 'notch' cadence, and giving staff full manual override capability. Off-the-shelf Shopify loyalty apps were either too rigid or too expensive for that level of customisation. The solution also had to live natively inside the Shopify admin — not as an external redirect — to keep the merchant experience seamless.

02 — The Strategy

Webhook-Driven App with Multi-Tenant Data Layer

The app was built with React Router 7 and the Shopify App React Router adapter, embedded directly in the admin using Shopify's Polaris web components. An `orders/paid` webhook handler automates notch calculation on every qualifying purchase, with a `ProcessedWebhook` deduplication table making retries safe. Prisma with PostgreSQL handles all data with strict shop-scoping for multi-tenancy, Supabase Storage handles receipt image uploads, and a Nodemailer abstraction layer dispatches email notifications fire-and-forget so SMTP failures never interrupt reward logic.

03 — The Impact

Fully Automated Reward Lifecycle, Zero Manual Tracking

The client received a loyalty system that requires zero manual intervention for notch tracking — rewards unlock automatically at threshold and the admin is emailed instantly. Staff retain full control via the embedded dashboard to issue and redeem rewards, make manual notch adjustments, and configure thresholds per shop. Customers get a self-service portal at `/apps/rewards` to check their progress, upload receipts for bonus notches, and claim social follow bonuses — reducing inbound support queries around reward status.

System Architecture

Webhook-Driven Rewards & Multi-Tenant Data Layer

Shopify orders/paid webhook → Notch calculation (Prisma/PostgreSQL) → Admin UI (React Router loaders) + Customer Portal (App Proxy)

Webhook & Business Logic

The `orders/paid` webhook is the single automated entry point for notch awarding. A `ProcessedWebhook` deduplication table ensures Shopify's retry mechanism never double-awards notches. All notch and reward logic lives in `rewards.server.ts`, keeping route files clean and business rules easy to modify without touching UI code.

Database Schema

Eight Prisma models cover the full reward lifecycle: `Customer` holds the live notch balance, `NotchEvent` is an immutable audit log of every change, `Reward` tracks progression from UNLOCKED through ISSUED to REDEEMED, and `RewardConfig` stores per-shop settings. Every table carries a `shop` field and every query filters by it, making the architecture multi-tenant safe by design.

Admin & Customer Surfaces

The embedded admin (React Router loaders and actions with Polaris web components) gives staff a metrics dashboard, a paginated customer list, and a per-customer detail view for manual adjustments and reward fulfillment. The customer portal is served through Shopify App Proxy at `/apps/rewards`, identifying the logged-in shopper via Shopify session without requiring a separate authentication flow.

Storage & Email Infrastructure

Receipt images are stored in a Supabase Storage bucket scoped by shop and customer ID, with public URLs recorded in the `PhotoUpload` model. Transactional emails are dispatched via a Nodemailer abstraction layer that is intentionally fire-and-forget — missing SMTP config degrades gracefully without crashing reward flows, and the transport can be swapped to SendGrid or Postmark by modifying a single file.

Lessons & Trade-offs

Multi-Tenancy Must Be Enforced at the Query Level

A single app instance serves multiple Shopify shops, so every database query must include an explicit `shop` filter — relying on session context alone is not sufficient. We enforced this by convention and code review throughout the project. In a larger team, a thin Prisma client wrapper that automatically injects the shop scope at query time would be a safer long-term approach than manual discipline.

Fire-and-Forget Emails Keep Webhooks Responsive

Shopify webhook handlers must respond within a tight timeout window. Dispatching email synchronously inside the handler risked both timeouts and blocking the reward database write. Decoupling email as fire-and-forget means SMTP failures never roll back committed rewards, and the webhook returns quickly. The accepted trade-off is that failed emails are surfaced through server logs rather than an automatic retry queue — acceptable for the current scale.

Idempotency Is Non-Negotiable for Webhooks

Shopify retries any webhook that does not receive a 2xx response, meaning the same `orders/paid` event can legitimately arrive multiple times. Without the `ProcessedWebhook` deduplication table, customers could silently accumulate double or triple the notches they earned — a data integrity bug that would be very difficult to diagnose post-launch. Treating idempotency as a first-class architectural requirement, not an afterthought, prevented this class of bug entirely.

Ready for the next deep dive?

Check out KMJ Catering & Events, a Client Website Build project.