Skip to content

Data Flow Patterns

UberLotto v2 uses five primary data flow patterns across the platform.

Pattern 1: SSR Page Load

Every page request flows through React Router loaders on the Oxygen edge runtime, fetching data in parallel from multiple sources before rendering HTML.

Browser Request


┌─────────────┐
│   Oxygen    │
│   Runtime   │
└──────┬──────┘


┌──────────────┐
│ React Router │      ┌──────────────┐
│   Loader     │─────▶│   Parallel   │
└──────────────┘      │   Fetches    │
                      └──────┬───────┘

          ┌──────────────────┼──────────────────┐
          ▼                  ▼                  ▼
   ┌────────────┐     ┌────────────┐     ┌────────────┐
   │  Shopify   │     │  Supabase  │     │  PostHog   │
   │ Storefront │     │    API     │     │ Analytics  │
   │    API     │     │            │     │            │
   └────────────┘     └────────────┘     └────────────┘
          │                  │                  │
          └──────────────────┼──────────────────┘

                    ┌──────────────┐
                    │   Rendered   │
                    │     HTML     │
                    └──────┬──────┘


                    ┌──────────────┐
                    │   Browser    │
                    │   Hydration  │
                    └──────────────┘

Key details:

  • Critical data (header, shop limits) is awaited before rendering — blocks time to first byte
  • Deferred data (footer, cart, customer profile) loads after initial render — non-blocking
  • Loaders use Promise.all() for parallel fetches
  • shouldRevalidate in root.tsx prevents unnecessary re-fetches on client navigation
  • Shopify data is cached using Hydrogen's built-in CacheLong() and CacheShort() strategies
typescript
// Example: root.tsx critical vs deferred loading
async function loadCriticalData({ context }) {
  const [header, shopLimits] = await Promise.all([
    storefront.query(HEADER_QUERY, { cache: storefront.CacheLong() }),
    getShopLimits(storefront),
  ]);
  return { header, shopLimits };
}

function loadDeferredData({ context }) {
  return {
    cart: cart.get(),                          // deferred
    isLoggedIn: customerAccount.isLoggedIn(),  // deferred
    footer: storefront.query(FOOTER_QUERY),    // deferred
  };
}

Pattern 2: Plisio Cryptocurrency Payment

Direct cryptocurrency payments using Plisio's invoice system with webhook-based confirmation. All transactions use the unified payment_transactions table.

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │     │  Oxygen  │     │ Supabase │     │  Plisio  │
└────┬─────┘     └────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │                │
     │ 1. Request     │                │                │
     │    Payment     │                │                │
     │    (auth req.) │                │                │
     │───────────────▶│                │                │
     │                │ 2. Save        │                │
     │                │    Pending Txn │                │
     │                │    (payment_   │                │
     │                │    transactions│                │
     │                │───────────────▶│                │
     │                │                │                │
     │                │ 3. Create      │                │
     │                │    Invoice     │                │
     │                │───────────────────────────────▶ │
     │                │                │                │
     │                │◀─────────────────────────────── │
     │                │    4. Invoice URL               │
     │                │                │                │
     │                │ 5. Update Txn  │                │
     │                │    w/ provider │                │
     │                │    _txn_id     │                │
     │                │───────────────▶│                │
     │                │                │                │
     │◀───────────────│                │                │
     │    6. Redirect │                │                │
     │    to Plisio   │                │                │
     │                │                │                │
     │                │                │   7. User Pays │
     │                │                │      Crypto    │
     │                │                │                │
     │                │◀──────────────────────────────  │
     │                │    8. Webhook Notification      │
     │                │                │                │
     │                │ 9. 6-Layer     │                │
     │                │    Security    │                │
     │                │    Pipeline    │                │
     │                │                │                │
     │                │ 10. Update Txn │                │
     │                │     in payment_│                │
     │                │     transactions                │
     │                │───────────────▶│                │
     │                │                │                │

Authentication: Customer must be logged in (customerAccount.isLoggedIn()) to create invoices and check status.

Security layers applied:

  1. Rate limiting — IP-based and global rate limits on webhook endpoint
  2. IP allowlistingPLISIO_WEBHOOK_IPS restricts webhook source IPs
  3. HMAC-SHA1 verification — Webhook signatures verified against PLISIO_SECRET_KEY
  4. Replay protection — Nonces stored in webhook_nonces table prevent duplicate processing
  5. Transaction rate limiting — Per-txn rate limit (10 req/min per txn_id)
  6. Duplicate check — Database-level dedup against payment_transactions

CORS: Invoice and status endpoints use origin-restricted getAllowedOrigin() (replaces wildcard *).

Endpoints: api.plisio-invoice.ts, api.plisio-webhook.ts, api.plisio-status.ts

Pattern 3: MoonPay Fiat-to-Crypto Payment

MoonPay provides a fiat on-ramp — users pay with credit card and receive crypto credited to their account. The complete pipeline includes transaction pre-creation, widget signing, and webhook-based processing into the unified payment_transactions table.

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │     │  Oxygen  │     │ Supabase │     │ MoonPay  │
└────┬─────┘     └────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │                │
     │ 1. Open        │                │                │
     │    MoonPay     │                │                │
     │    Widget      │                │                │
     │                │                │                │
     │ 2. Sign URL    │                │                │
     │    (auth req.) │                │                │
     │───────────────▶│                │                │
     │                │                │                │
     │                │ 3. Pre-create  │                │
     │                │    pending txn │                │
     │                │    (payment_   │                │
     │                │    transactions│                │
     │                │───────────────▶│                │
     │                │                │                │
     │                │ 4. HMAC sign   │                │
     │                │    URL query   │                │
     │◀───────────────│                │                │
     │    5. Signed   │                │                │
     │       URL      │                │                │
     │                │                │                │
     │ 6. User completes purchase      │                │
     │    in MoonPay widget ──────────────────────────▶ │
     │                │                │                │
     │                │◀──────────────────────────────  │
     │                │  7. Webhook: transaction_updated│
     │                │                │                │
     │                │ 8. Security    │                │
     │                │    Pipeline:   │                │
     │                │    Rate limit  │                │
     │                │    → HMAC-256  │                │
     │                │    → Timestamp │                │
     │                │    → Replay    │                │
     │                │    → Validate  │                │
     │                │                │                │
     │                │ 9. Find-or-    │                │
     │                │    create txn  │                │
     │                │    a) by       │                │
     │                │    provider_   │                │
     │                │    txn_id      │                │
     │                │    b) by       │                │
     │                │    order_number│                │
     │                │    c) create   │                │
     │                │    new         │                │
     │                │                │                │
     │                │ 10. Update     │                │
     │                │     status in  │                │
     │                │     payment_   │                │
     │                │     transactions                │
     │                │───────────────▶│                │
     │                │                │                │
     │                │ 11. Log        │                │
     │                │     security   │                │
     │                │     event      │                │
     │                │───────────────▶│                │
     │                │                │                │

Key details:

  • MoonPay widget is embedded via @moonpay/moonpay-react (MoonPayCheckout component)
  • Authentication required: Sign endpoint requires customerAccount.isLoggedIn()
  • Transaction pre-creation: The sign endpoint (api.moonpay-sign.ts) pre-creates a pending payment_transactions record using URL parameters (externalTransactionIdorder_number, externalCustomerIduser_email). This allows the webhook to match incoming notifications to existing records.
  • HMAC-SHA256 verification: Webhooks use Moonpay-Signature-V2 header with format t=TIMESTAMP,s=SIGNATURE. Signs timestamp.body, hex-encoded.
  • Timestamp freshness: Rejects webhooks older than 5 minutes
  • Replay protection: Nonce-based via checkGenericWebhookNonce() using shared webhook_nonces table with 5-min TTL
  • Transaction lookup: Three-step strategy — by provider_txn_id, then by order_number, then create new
  • Payload handling: Handles MoonPay sending data as a JSON string (double-parse)
  • Webhook handler returns 200 even on errors to prevent MoonPay retries (per their best practice)
  • CORS: Sign endpoint uses origin-restricted getAllowedOrigin() (replaces wildcard *)

Endpoints: api.moonpay-sign.ts, api.moonpay-webhook.ts

Pattern 4: Shopify Checkout Credit Loading

Users can load UBL Points (credits) by purchasing products through standard Shopify checkout.

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │     │  Oxygen  │     │ Shopify  │     │ Shopify  │
│          │     │          │     │Storefront│     │ Checkout │
└────┬─────┘     └────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │                │
     │ 1. Select      │                │                │
     │    Credit Amt  │                │                │
     │───────────────▶│                │                │
     │                │ 2. Verify Auth │                │
     │                │    (Customer   │                │
     │                │     Account)   │                │
     │                │                │                │
     │                │ 3. Fetch       │                │
     │                │    Variants    │                │
     │                │───────────────▶│                │
     │                │◀───────────────│                │
     │                │                │                │
     │                │ 4. Validate    │                │
     │                │    Amount &    │                │
     │                │    Price Match │                │
     │                │                │                │
     │                │ 5. Create Cart │                │
     │                │    (cartCreate)│                │
     │                │───────────────▶│                │
     │                │◀───────────────│                │
     │                │   checkoutUrl  │                │
     │                │                │                │
     │◀───────────────│                │                │
     │  6. Redirect   │                │                │
     │     to Checkout│                │                │
     │─────────────────────────────────────────────────▶│
     │                │                │    7. User     │
     │                │                │       Pays     │
     │                │                │                │

Key details:

  • "Load Credits" collection in Shopify contains UBL Point products at fixed amounts
  • Server-side api.shopify-checkout.ts endpoint handles checkout creation
  • Authentication requiredcustomerAccount.handleAuthStatus() checked before processing
  • Price validation — variant price from Shopify is compared against requested amount to prevent manipulation (1 cent tolerance)
  • CSRF protection — origin/referer header validation against allowed domains
  • Input validation — Content-Type, body size (1KB max), and structured field validation

Files: api.shopify-checkout.ts, utils/shopify-checkout.server.ts, graphql/load-credits/LoadCreditsQuery.ts

Pattern 5: Product-to-Jackpot Data Enrichment

Lottery products from Shopify are enriched with real-time jackpot data from Supabase using a slug-based matching system.

┌─────────────────────────────────────────────────────────┐
│                 Shopify Product                          │
│  ┌───────────────────────────────────────────────────┐  │
│  │  Product: "Powerball Pool"                        │  │
│  │  Metafield: custom.game_slug = "powerball"        │  │
│  └──────────────────────┬────────────────────────────┘  │
└─────────────────────────┼───────────────────────────────┘

                          │  slug = "powerball"


┌─────────────────────────────────────────────────────────┐
│              LotteryProductEnrichmentService             │
│  ┌───────────────────────────────────────────────────┐  │
│  │  1. Build Map<slug, JackpotDrawing> from drawings │  │
│  │  2. Match product.gameSlug → drawingsMap.get()    │  │
│  │  3. Calculate next drawing date from schedule     │  │
│  │  4. Return EnrichedLotteryProduct                 │  │
│  └──────────────────────┬────────────────────────────┘  │
└─────────────────────────┼───────────────────────────────┘

              ┌───────────┴───────────┐
              ▼                       ▼
┌──────────────────────┐  ┌──────────────────────┐
│  Supabase: jackpots  │  │  Supabase:           │
│  ┌────────────────┐  │  │  past_drawings       │
│  │ slug           │  │  │  ┌────────────────┐  │
│  │ game_name      │  │  │  │ slug           │  │
│  │ jackpot_numeric│  │  │  │ game_name      │  │
│  │ last_updated   │  │  │  │ draw_date      │  │
│  └────────────────┘  │  │  │ winning_numbers│  │
└──────────────────────┘  │  └────────────────┘  │
                          └──────────────────────┘

Key details:

  • Single identifier system — the custom.game_slug Shopify metafield is the only connection between products and Supabase data
  • LotteryProductEnrichmentService creates an O(1) lookup map from drawings array
  • Products are deduplicated by ID during batch enrichment
  • Next drawing dates are calculated from product schedule metafields (drawGamesSchedule, lotteryPoolCutoffTime)
  • This service is shared between Home and Collections pages to eliminate code duplication

Files: services/lottery-product-enrichment.service.ts, utils/jackpot.server.ts, utils/pastDrawings.server.ts

UberLotto Technical Documentation