Loyalty Platform Architecture

Digital stamp & points system with Google Wallet and Apple Wallet support

1. High-Level Overview

This document describes a modular loyalty platform that supports:

The architecture is designed to be extensible: you can add tiers, referrals, coupons, gift cards, and more without redesigning the core.

2. Backend Architecture

2.1 Core Modules

2.2 Folder Structure

backend/
  app/
    main.py
    config.py
    dependencies.py

    api/
      merchants.py
      stores.py
      passes.py
      stamps.py
      points.py
      qr.py
      dashboard.py
      apple_passes.py

    core/
      google_wallet.py
      loyalty_engine/
        stamps.py
        points.py
      qr_generator.py
      apple_wallet/
        __init__.py
        signer.py
        generator.py
        templates/
          loyalty_pass.json
          images/
            icon.png
            logo.png
            strip.png

    models/
      merchant.py
      store.py
      customer.py
      pass_object.py
      stamp_event.py
      points_event.py
      apple_device_registration.py

    db/
      base.py
      session.py
      migrations/

    utils/
      security.py
      id_generator.py
      validators.py

  requirements.txt

3. Data Model

3.1 Merchant vs Store

Merchant = business entity (e.g., “CoffeeLux”) Store = specific location/branch (e.g., “CoffeeLux Gare”).

Merchant

FieldType
idUUID
namestring
emailstring
password_hashstring
created_atdatetime

Store

FieldType
idUUID
merchant_idFK → Merchant
namestring
qr_code_urlstring
loyalty_type"stamps" | "points" | "hybrid"
staff_pin_hashstring (hashed)

Customer

FieldType
idUUID
emailstring (nullable)
phonestring (nullable)
email_consentbool
email_consent_atdatetime (nullable)
created_atdatetime

PassObject

FieldType
idUUID
customer_idFK → Customer
store_idFK → Store
google_pass_idstring (nullable)
apple_serialstring (nullable)
apple_auth_tokenstring (nullable)
stamp_countint
points_balanceint
reward_unlockedbool
last_stamp_atdatetime (nullable)
last_points_atdatetime (nullable)

StampEvent

FieldType
idUUID
pass_idFK → PassObject
store_idFK → Store
timestampdatetime

PointsEvent

FieldType
idUUID
pass_idFK → PassObject
store_idFK → Store
points_addedint
amountdecimal (optional)
timestampdatetime

AppleDeviceRegistration

FieldType
idUUID
device_idstring
pass_serialstring
push_tokenstring
updated_atdatetime

4. Core Flows

4.1 Creating a Pass (Onboarding)

  1. Customer scans a QR code or visits a link.
  2. Page asks for optional data:
    • Name (optional)
    • Email (optional, with consent checkbox)
    • Phone (optional)
  3. Backend:
    • Creates or finds Customer.
    • Creates PassObject for the chosen store.
    • Generates Google Wallet link and/or Apple Wallet .pkpass.
  4. Customer taps “Add to Google Wallet” or “Add to Apple Wallet”.

4.2 Stamping a Pass (Visits)

Endpoint: POST /stamp/{store_id}

  1. Customer scans store QR: https://yourdomain.com/stamp/{store_id}.
  2. Page identifies the customer’s pass (via login, token, or Wallet ID).
  3. Staff enters a PIN on the customer’s device.
  4. Backend:
    • Validates staff PIN.
    • Checks cooldown (e.g., 1 stamp per X minutes).
    • Increments stamp_count.
    • Logs a StampEvent.
    • Updates Google Wallet and Apple Wallet passes.
  5. Customer sees updated stamp count.

4.3 Adding Points (Purchases)

Endpoint: POST /points/{store_id}

{
  "pass_id": "abc123",
  "amount": 59.90,
  "staff_pin": "4821"
}
  1. Staff enters purchase amount and PIN.
  2. Backend:
    • Validates staff PIN.
    • Converts amount → points (e.g., €1 = 1 point).
    • Updates points_balance.
    • Logs a PointsEvent.
    • Updates Wallet passes.

4.4 Reward Logic

Implemented in loyalty_engine/stamps.py and loyalty_engine/points.py.

5. Staff PIN System & Anti-Fraud

5.1 Staff PIN Concept

Each store has one or more staff PINs. A stamp or points addition is only valid if a correct PIN is provided.

Example request:

POST /stamp/{store_id}
{
  "pass_id": "abc123",
  "staff_pin": "4821"
}

5.2 Implementation

5.3 Additional Anti-Fraud Layers

The QR code itself never directly grants a stamp or points. It only opens a URL; the backend decides whether to award anything.

6. Email Collection & GDPR Considerations

6.1 When to Collect Email

6.2 Data Model

6.3 GDPR-Friendly Flow (EU/Luxembourg)

6.4 Usage

7. Features Beyond Stamps & Points

7.1 Reward Types

7.2 Coupons, Vouchers, Gift Cards

7.3 Marketing & Communication

7.4 Analytics & CRM

7.5 Advanced Options

8. Google Wallet Integration

8.1 Core Concepts

8.2 Required Setup

8.3 Backend Responsibilities

8.4 Example Endpoint Sketch

@router.post("/passes/create")
async def create_pass(data: PassCreateRequest, db: Session = Depends(get_db)):
    customer = get_or_create_customer(db, data)
    pass_obj = get_or_create_pass(db, customer, data.store_id)

    google_link = google_wallet.create_pass(pass_obj)
    return {"add_to_wallet_url": google_link}

9. Apple Wallet Integration

9.1 Core Difference

Apple Wallet uses signed .pkpass files instead of a JSON API. A .pkpass is a ZIP containing:

9.2 Requirements

9.3 Pass Template (pass.json)

{
  "formatVersion": 1,
  "passTypeIdentifier": "pass.com.yourbrand.loyalty",
  "teamIdentifier": "ABCDE12345",
  "organizationName": "Your Brand",
  "serialNumber": "{{serial}}",
  "description": "Loyalty Card",
  "logoText": "Your Brand",
  "foregroundColor": "rgb(255,255,255)",
  "backgroundColor": "rgb(0,0,0)",
  "storeCard": {
    "primaryFields": [
      {
        "key": "points",
        "label": "Points",
        "value": "{{points}}"
      }
    ],
    "secondaryFields": [
      {
        "key": "stamps",
        "label": "Stamps",
        "value": "{{stamps}}"
      }
    ]
  },
  "barcode": {
    "format": "PKBarcodeFormatQR",
    "message": "{{pass_id}}",
    "messageEncoding": "iso-8859-1"
  },
  "webServiceURL": "https://yourdomain.com/apple/updates",
  "authenticationToken": "{{auth_token}}"
}

9.4 pkpass Generation Flow

  1. Load loyalty_pass.json template.
  2. Inject dynamic values (serial, points, stamps, auth token, etc.).
  3. Add required images (icon, logo, strip).
  4. Create manifest.json with SHA-1 hashes of all files.
  5. Sign manifest with your .p12 certificate and Apple’s WWDR cert.
  6. Zip everything into a .pkpass file.
  7. Return .pkpass from endpoint GET /passes/apple/{pass_id}.

9.5 Device Registration & Updates

Apple Wallet uses a push + pull model for updates:

  1. Customer adds pass → Apple sends device registration to your backend.
  2. You store device ID, pass serial, and push token in AppleDeviceRegistration.
  3. When stamps/points change:
    • You send a push notification to Apple using the push token.
    • Apple notifies the device; Wallet calls your webServiceURL.
    • Device fetches updated pass data from GET /apple/updates/{pass_type}/{serial}.

9.6 Data Model Additions

9.7 What Stays the Same

10. Multi-Wallet Strategy

Your platform supports both Google Wallet and Apple Wallet by treating them as two output formats for the same underlying PassObject.

Whenever stamps or points change, you:

The merchant and store don’t need to care about the technical differences—your backend abstracts it away.

11. Deployment & Stack

12. MVP vs Future Phases

12.1 MVP Scope

12.2 Phase 2 Ideas

13. Closing Notes

The key design choice is that you do not redesign the system when adding:

You extend the same core models and flows with additional modules and fields, keeping the architecture modular and future-proof.