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 shouldRevalidateinroot.tsxprevents unnecessary re-fetches on client navigation- Shopify data is cached using Hydrogen's built-in
CacheLong()andCacheShort()strategies
// 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:
- Rate limiting — IP-based and global rate limits on webhook endpoint
- IP allowlisting —
PLISIO_WEBHOOK_IPSrestricts webhook source IPs - HMAC-SHA1 verification — Webhook signatures verified against
PLISIO_SECRET_KEY - Replay protection — Nonces stored in
webhook_noncestable prevent duplicate processing - Transaction rate limiting — Per-txn rate limit (10 req/min per txn_id)
- 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(MoonPayCheckoutcomponent) - Authentication required: Sign endpoint requires
customerAccount.isLoggedIn() - Transaction pre-creation: The sign endpoint (
api.moonpay-sign.ts) pre-creates a pendingpayment_transactionsrecord using URL parameters (externalTransactionId→order_number,externalCustomerId→user_email). This allows the webhook to match incoming notifications to existing records. - HMAC-SHA256 verification: Webhooks use
Moonpay-Signature-V2header with formatt=TIMESTAMP,s=SIGNATURE. Signstimestamp.body, hex-encoded. - Timestamp freshness: Rejects webhooks older than 5 minutes
- Replay protection: Nonce-based via
checkGenericWebhookNonce()using sharedwebhook_noncestable with 5-min TTL - Transaction lookup: Three-step strategy — by
provider_txn_id, then byorder_number, then create new - Payload handling: Handles MoonPay sending
dataas 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.tsendpoint handles checkout creation - Authentication required —
customerAccount.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_slugShopify metafield is the only connection between products and Supabase data LotteryProductEnrichmentServicecreates 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