Skip to main content

Config Caching Strategy

Store Shield uses a 4-layer caching strategy to serve protection configuration with minimal latency and cost. This document explains why and how.

The Problem

Every page load on a merchant's store needs to know:

  • Which protections are enabled (right-click, copy, bot detection, etc.)
  • IP blocking rules
  • Current plan features

Naive approach: Query Fly.io database on every request
Problem: High latency, expensive, single point of failure

The Solution: 4-Layer Waterfall

Request arrives


┌─────────────────────────────────────────────────────┐
│ Layer 1: MEMORY CACHE (Worker isolate) │
│ • Fastest (nanoseconds) │
│ • Per-worker, lives until isolate recycles │
│ • Hit rate: ~60-70% │
└─────────────────────────┬───────────────────────────┘
│ miss

┌─────────────────────────────────────────────────────┐
│ Layer 2: KV CACHE (Cloudflare KV) │
│ • Fast (~10-50ms) │
│ • Global, 5-minute TTL │
│ • Hit rate: ~25-30% │
└─────────────────────────┬───────────────────────────┘
│ miss

┌─────────────────────────────────────────────────────┐
│ Layer 3: D1 CACHE (Cloudflare D1) │
│ • Moderate (~20-100ms) │
│ • Edge-local, persistent │
│ • Hit rate: ~5-8% │
└─────────────────────────┬───────────────────────────┘
│ miss

┌─────────────────────────────────────────────────────┐
│ Layer 4: SOURCE (Fly.io API) │
│ • Slowest (~100-300ms) │
│ • Always current, source of truth │
│ • Hit rate: ~1-2% │
└─────────────────────────────────────────────────────┘

Implementation

Layer 1: Memory Cache

// In-memory cache per Worker isolate
const configCache = new Map();

async function getConfig(shop) {
const cacheKey = `config:${shop}`;

// Layer 1: Memory
if (configCache.has(cacheKey)) {
const { config, timestamp } = configCache.get(cacheKey);
if (Date.now() - timestamp < 60_000) { // 1 minute
return config;
}
}

// Continue to Layer 2...
}

Layer 2: KV Cache

async function getConfig(shop, env) {
// ... Layer 1 miss ...

// Layer 2: KV
const kvKey = `merchant-config:${shop}`;
const kvConfig = await env.KV.get(kvKey, 'json');

if (kvConfig) {
// Populate Layer 1
configCache.set(cacheKey, { config: kvConfig, timestamp: Date.now() });
return kvConfig;
}

// Continue to Layer 3...
}

Layer 3: D1 Cache

async function getConfig(shop, env) {
// ... Layer 2 miss ...

// Layer 3: D1
const d1Config = await env.DB.prepare(
'SELECT * FROM MerchantConfig WHERE shop = ?'
).bind(shop).first();

if (d1Config) {
// Populate Layers 1 & 2
configCache.set(cacheKey, { config: d1Config, timestamp: Date.now() });
await env.KV.put(kvKey, JSON.stringify(d1Config), { expirationTtl: 300 });
return d1Config;
}

// Continue to Layer 4...
}

Layer 4: Source (Fly.io)

async function getConfig(shop, env) {
// ... Layer 3 miss ...

// Layer 4: Fly.io (source of truth)
const response = await fetch(`${FLY_API_URL}/api/protection-config?shop=${shop}`);
const sourceConfig = await response.json();

// Populate all layers
configCache.set(cacheKey, { config: sourceConfig, timestamp: Date.now() });
await env.KV.put(kvKey, JSON.stringify(sourceConfig), { expirationTtl: 300 });
await env.DB.prepare(
'INSERT OR REPLACE INTO MerchantConfig (shop, config, updated_at) VALUES (?, ?, ?)'
).bind(shop, JSON.stringify(sourceConfig), Date.now()).run();

return sourceConfig;
}

Cache Invalidation

On Settings Change

When a merchant updates settings:

// app/routes/api.protection.config.ts
export async function action({ request }) {
// 1. Save to Prisma (source of truth)
await prisma.merchantSettings.update({
where: { shop },
data: newSettings
});

// 2. Invalidate edge caches
await fetch(`${EVENTS_WORKER_URL}/api/invalidate-config`, {
method: 'POST',
body: JSON.stringify({ shop })
});
}

Heartbeat-Based Updates

The theme extension sends heartbeats that naturally refresh the cache:

// heartbeat-handler.js
export async function handleHeartbeat(request, env) {
const { shop, protections } = await request.json();

// Update D1 with current config
await env.DB.prepare(`
INSERT OR REPLACE INTO MerchantConfig (shop, protections, last_seen)
VALUES (?, ?, ?)
`).bind(shop, JSON.stringify(protections), Date.now()).run();

// KV will naturally expire and refresh
}

Cost Analysis

LayerCost per ReadReads/month (est.)Monthly Cost
Memory$06M$0
KV$0.50/M2.5M~$1.25
D1$0.001/M500K~$0.50
Fly.io~$0.01/req100K~$1.00
Total~$2.75

Without caching (all requests to Fly.io): ~$90/month

Savings: ~97%

TTL Strategy

LayerTTLReason
Memory1 minuteFresh enough, isolate may recycle anyway
KV5 minutesBalance freshness vs cost
D1IndefinitePersistent backup, overwritten on update

Trade-offs

Advantages

  1. Cost: 97% reduction in origin requests
  2. Latency: Most requests served in under 10ms
  3. Resilience: Works even if Fly.io is down (D1 fallback)
  4. Scalability: Edge handles any traffic spike

Disadvantages

  1. Staleness: Config changes take up to 5 minutes to propagate
  2. Complexity: 4 layers to debug
  3. Inconsistency: Different edge locations may have different cache states

Mitigation

  • Explicit invalidation on settings change
  • Heartbeat naturally refreshes active stores
  • Graceful degradation with last-known config