Security Architecture
UberLotto implements a 3-layer defense-in-depth security model spanning network, application, and data layers. All security-related types and enums are defined in the canonical source file app/lib/security-types.ts.
Security Layers
┌──────────────────────────────────────────────────────────────────┐
│ SECURITY LAYERS │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: NETWORK │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Supabase Network Restrictions (IP Whitelisting) │ │
│ │ • Cloudflare DDoS Protection (via Shopify Oxygen) │ │
│ │ • HTTPS/TLS Encryption │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 2: APPLICATION │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Rate Limiting (per-txn, per-IP, global circuit breaker)│ │
│ │ • Input & Amount Validation │ │
│ │ • CSRF Protection (origin-based) │ │
│ │ • Webhook HMAC Signature Verification │ │
│ │ • Endpoint Authentication (Shopify Customer Account) │ │
│ │ • CORS Origin Restriction (getAllowedOrigin) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 3: DATA │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ • Row Level Security (RLS) in PostgreSQL │ │
│ │ • Replay Attack Prevention (nonce tracking) │ │
│ │ • GDPR-Compliant Security Event Logging │ │
│ │ • Data Encryption at Rest (Supabase-managed) │ │
│ │ • DB-Level Triggers (immutable completed, max events) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘Network Security
Supabase Network Restrictions
Direct PostgreSQL connections are restricted to whitelisted IP addresses only.
| Type | CIDRs | Description |
|---|---|---|
| Developer IP | 178.148.227.175/32 | Local development access |
| Cloudflare IPv4 | 15 ranges | Shopify Oxygen edge servers |
| Cloudflare IPv6 | 7 ranges | Shopify Oxygen edge servers |
Important Limitation
Network Restrictions only apply to direct PostgreSQL connections. They do NOT restrict:
- REST API (PostgREST)
- Auth API
- Storage API
For API security, use Row Level Security (RLS) policies.
Cloudflare IP Ranges (Whitelisted)
IPv4:
173.245.48.0/20 103.21.244.0/22 103.22.200.0/22
103.31.4.0/22 141.101.64.0/18 108.162.192.0/18
190.93.240.0/20 188.114.96.0/20 197.234.240.0/22
198.41.128.0/17 162.158.0.0/15 104.16.0.0/13
104.24.0.0/14 172.64.0.0/13 131.0.72.0/22IPv6:
2400:cb00::/32 2606:4700::/32 2803:f800::/32
2405:b500::/32 2405:8100::/32 2a06:98c0::/29
2c0f:f248::/32Managing Restrictions
# View current restrictions
supabase network-restrictions --project-ref <ref> get --experimental
# Update restrictions
supabase network-restrictions --project-ref <ref> update \
--db-allow-cidr YOUR_IP/32 \
--experimentalApplication Security
Rate Limiting
File: app/lib/rate-limiter.server.ts
Three-tier in-memory rate limiting with sliding window algorithm:
| Tier | Scope | Limit | Window |
|---|---|---|---|
| Transaction | Per transaction ID | 10 req | 1 min |
| IP Address | Per client IP | 100 req | 1 min |
| Global Circuit Breaker | All requests | 1000 req | 1 min |
Additional invoice creation limits (enforced in api.plisio-invoice.ts):
- Max 5 pending transactions per user email
- Max 3 new invoices per minute per user email
See Rate Limiting Configuration for full details.
Webhook Security
Files:
app/lib/webhook-validator.server.ts— HMAC verification, IP whitelistingapp/lib/webhook-processor.server.ts— Business logic, amount/currency validationapp/lib/webhook-extractor.server.ts— Multi-format data extractionapp/lib/replay-protection.server.ts— Nonce-based replay prevention
HMAC Signature Verification
Plisio webhooks are verified using HMAC-SHA1 with PHP serialize format:
Correction
The signature comparison uses a custom XOR-based constantTimeEqual() function, NOT crypto.timingSafeEqual. This is because the Web Crypto API used in Cloudflare Workers does not provide timingSafeEqual.
// Custom constant-time comparison (XOR-based)
private constantTimeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}MoonPay webhooks use HMAC-SHA256 with timestamp freshness validation:
- Header format:
Moonpay-Signature-V2: t=TIMESTAMP,s=SIGNATURE - Signed payload:
TIMESTAMP.BODY(timestamp + literal period + raw body) - Signature encoding: hex
- Timestamp freshness: rejects webhooks older than 5 minutes
- Replay protection: nonce-based via
checkGenericWebhookNonce()using sharedwebhook_noncestable with 5-minute TTL - Same XOR-based constant-time comparison pattern as Plisio
See Webhook Security Deep Dive for full pipeline details.
Endpoint Authentication
Several payment API endpoints require Shopify Customer Account authentication via customerAccount.isLoggedIn(). Unauthenticated requests receive a 401 Unauthorized response.
| Endpoint | Auth Required |
|---|---|
POST /api/plisio-invoice | Yes — customerAccount.isLoggedIn() |
GET /api/plisio-status | Yes — customerAccount.isLoggedIn() |
GET/POST /api/wallet-transactions | Yes — customerAccount.isLoggedIn() |
POST /api/moonpay-sign | Yes — customerAccount.isLoggedIn() |
POST /api/shopify-checkout | Yes — customerAccount.handleAuthStatus() |
POST /api/moonpay-webhook | No (server-to-server, HMAC-verified) |
GET/POST /api/plisio-webhook | No (server-to-server, HMAC-verified) |
CORS Origin Restriction
File: app/lib/cors.server.ts
All payment API endpoints use a shared getAllowedOrigin() function that restricts CORS origins:
- Development: Allows
localhostand127.0.0.1(any port) - Production: Only allows the app's own origin (same-origin)
Requests from disallowed origins receive no Access-Control-Allow-Origin header (or 403 Forbidden on OPTIONS preflight). This replaces the previous wildcard * CORS policy.
Endpoints using getAllowedOrigin(): wallet-transactions, plisio-invoice, plisio-status, moonpay-sign.
Input Validation
File: app/lib/amount-validator.server.ts
All payment amounts are validated against:
- NaN / Infinity rejection
- Scientific notation blocking
- Precision exploit prevention
- Range bounds checking
Data Security
Row Level Security (RLS)
All Supabase tables have RLS enabled. Security tables restrict access to the service role only:
-- Only service role can insert webhook nonces
CREATE POLICY "Service role can insert nonces"
ON webhook_nonces FOR INSERT TO service_role
WITH CHECK (true);
-- Only service role can read security events
CREATE POLICY "Service role can read events"
ON security_events FOR SELECT TO service_role
USING (true);Security Database Tables
webhook_nonces — Replay Attack Prevention
Correction from Manual Docs
Primary keys use BIGSERIAL (auto-incrementing integer), NOT UUID.
CREATE TABLE webhook_nonces (
id BIGSERIAL PRIMARY KEY,
nonce_hash TEXT UNIQUE NOT NULL,
txn_id TEXT NOT NULL,
status TEXT,
amount TEXT,
order_number TEXT,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);security_events — Audit Logging
CREATE TABLE security_events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
severity TEXT NOT NULL,
source TEXT NOT NULL,
client_ip TEXT,
user_email TEXT,
txn_id TEXT,
order_number TEXT,
amount NUMERIC,
currency TEXT,
user_agent TEXT,
status TEXT NOT NULL,
error_message TEXT,
event_data JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);Database-Level Protections
The payment_transactions table includes PostgreSQL trigger-based protections:
| Trigger | Purpose |
|---|---|
| Immutable completed | Prevents updates to transactions with completed status — ensures payment records cannot be modified after finalization |
| Max events limiter | Limits the number of payment_transaction_events per transaction to prevent unbounded event accumulation |
| Cancellation rate limiter | Rate-limits cancellation operations to prevent abuse |
These triggers operate at the database level, providing defense-in-depth protection independent of application code.
Replay Protection Nonce Formula
Two nonce functions are available:
- Plisio (specific):
checkWebhookNonce()—SHA256(txn_id:status:amount:order_number)with colon separators - Generic (any provider):
checkGenericWebhookNonce()—SHA256(rawNonceString)where the caller provides the raw string
Correction from Manual Docs
The Plisio nonce formula is SHA256(txn_id:status:amount:order_number) with colon separators — NOT SHA256(txn_id + status + amount + timestamp).
MoonPay uses the generic function with nonce string: SHA256(moonpay_txn_id:status:event_type).
Both functions share the same webhook_nonces table with 5-minute TTL and fail-open behavior.
Security Event Types
The canonical list of event types is defined in app/lib/security-types.ts:
| Event Type | Severity | Description |
|---|---|---|
webhook_received | info | Webhook successfully received |
hmac_failure | critical | HMAC signature verification failed |
replay_detected | critical | Duplicate webhook nonce detected |
rate_limit_violation | warning | Rate limit exceeded |
ip_whitelist_violation | critical | Request from non-whitelisted IP |
payment_success | info | Payment completed successfully |
payment_failure | error | Payment processing failed |
amount_validation_failed | warning | Amount validation rejected |
currency_mismatch | warning | Unexpected source currency |
Security Event Sources
| Source | Module |
|---|---|
webhook_validator | app/lib/webhook-validator.server.ts |
webhook_processor | app/lib/webhook-processor.server.ts |
rate_limiter | app/lib/rate-limiter.server.ts |
payment_validator | Amount / currency validation |
replay_protection | app/lib/replay-protection.server.ts |
ip_validator | IP whitelist checks |
invoice_handler | app/routes/api.plisio-invoice.ts |
API Key Security
| Key | Exposure | Use Case |
|---|---|---|
SUPABASE_ANON_KEY | Public (client) | Client-side queries (limited by RLS) |
SUPABASE_SERVICE_ROLE_KEY | PRIVATE | Server-side operations (bypasses RLS) |
PLISIO_API_KEY | PRIVATE | Payment gateway API calls |
PLISIO_SECRET_KEY | PRIVATE | Webhook HMAC verification |
MOONPAY_SECRET_KEY | PRIVATE | MoonPay URL signing |
MOONPAY_WEBHOOK_KEY | PRIVATE | MoonPay webhook verification |
Key Storage
| Environment | Storage |
|---|---|
| Local | .env file (gitignored) |
| Production | Shopify Oxygen environment variables |
| CI/CD | GitHub Secrets |
Monitoring
Critical Event Queries
-- Critical events in last 24 hours
SELECT * FROM security_events
WHERE severity = 'critical'
AND created_at > NOW() - INTERVAL '24 hours'
ORDER BY created_at DESC;
-- Rate limit violations by IP
SELECT client_ip, COUNT(*) as hit_count, MAX(created_at) as last_hit
FROM security_events
WHERE event_type = 'rate_limit_violation'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY client_ip
ORDER BY hit_count DESC;Alerting Thresholds
Set up alerts for:
severity = 'critical'events (HMAC failures, replay attacks, IP violations)- High volume of rate limit violations from a single IP
- Multiple
payment_failureevents in short succession
Compliance Notes
GDPR Data Handling
- PII Masking: Emails are masked in security logs (e.g.,
te***t@example.com) - IP Masking: IPs are partially masked (e.g.,
192.168.xxx.xxx) - Retention: Security events retained for 365 days
- Data Purpose: Logged data is used for security monitoring only
- Deletion: Users can request data deletion
Incident Response
- Identify — Check
security_eventstable for anomalies - Contain — Block suspicious IPs via network restrictions
- Eradicate — Fix the vulnerability
- Recover — Restore normal operation
- Learn — Update security measures and documentation