Skip to content

Paid access & entitlements

A paying customer is one whose requests match the entitlement allow-list. When they match, the watermark decision short-circuits to clean. The allow-list is kept current automatically from Stripe subscription webhooks, so onboarding a customer does not require a code change or a deploy.

A customer subscribes through Stripe Checkout and, in a checkout custom field, lists the domains and/or IPs their thumbnails are served from. On a successful checkout, Vumbnail records the entitlement and the customer’s traffic starts getting clean images within the allow-list’s propagation window.

For each request the Worker checks the allow-list in this order and stops at the first match:

  1. API key — from the X-Vumbnail-Key header or ?key= query param (keys look like vb_live_<label>_<random>).
  2. Client IP — from CF-Connecting-IP.
  3. Referer host — lowercased, www./m. prefixes ignored.
  4. User-agent substring — for mobile apps that can’t set a referrer.

Any match ⇒ variant = clean.

The allow-list has two read paths and one write path:

  • Write: the accounts Worker (accounts.vumbnail.com) receives Stripe webhooks (checkout.session.completed, checkout.session.async_payment_succeeded), parses the domains/IPs, and upserts a customer record. It writes to three stores: a committed customers.json (via the GitHub API), a D1 database (the record of truth), and a regenerated KV projection.
  • Read (runtime): the thumbnail Worker reads the KV projection (billing-entitlements:projection) on the hot path, cached briefly in the isolate. This is the live path — new customers propagate without a deploy.
  • Read (fallback): if KV is unavailable, the Worker falls back to the customers.json bundled at deploy time.

The entitlement system runs in runtime mode (KV projection preferred), having graduated from static (bundled JSON) → shadow (compare-and-log) → runtime.

Inputs: Stripe webhook events with a domains custom field; per-request X-Vumbnail-Key/?key=, CF-Connecting-IP, Referer, User-Agent.

Outputs: an updated entitlement projection in KV + D1 + customers.json; a per-request bypass boolean feeding the watermark decision; in debug mode, x-thumbnail-billing-entitlements-mode and bypass headers.

  • Duplicate webhook — events are de-duplicated in KV (PROCESSED_EVENTS_STORE, ~90-day TTL) so a re-delivered event doesn’t double-write.
  • KV read failure — falls back to the bundled customers.json; the request is still served, defaulting to clean only if a static entry matches.
  • Propagation delay — a newly paid customer can briefly still see watermarks until the projection refreshes (KV cache TTL). The acceptable window is an open question (see below).
  • Revocation — an entitlement removed from the projection stops bypassing at the next cache refresh; there is no instant sub-minute revoke path today.
  • Unmatched paid traffic — if a customer serves from a domain/IP they didn’t list, those requests are treated as free and may be watermarked; the fix is to add the domain/IP to their record.
// Per-subscription customer record (customers.json + D1 + KV projection source)
type CustomerAllowlistRecord = {
stripeSubscriptionId: string // primary key
stripeCustomerId: string | null
stripePriceId: string | null
stripeProductId: string | null
customerEmail: string | null
domains: string[] // referer hosts → clean
ips: string[] // client IPs → clean
addedAt: string
updatedAt: string
}
// The hot-path projection the thumbnail Worker reads from KV
type BillingEntitlementsProjection = {
kind: "billing-entitlements/v1"
version: string
generatedAt: string
customerCount: number
domains: string[]
ips: string[]
apiKeys: string[]
userAgents: string[]
}

KV keys: billing-entitlements:mode, billing-entitlements:projection. D1 tables: billing_entitlements_current (latest per subscription), billing_entitlement_events (append-only audit).

  • Self-serve onboarding. Today the paid set is small and partly hand-onboarded. The end-to-end “subscribe → list domains → clean in minutes” self-serve flow and its quickstart docs are planned, not shipped.
  • Projection freshness vs. cost. Raising the KV cache TTL (e.g. 60s → 300s) cuts KV reads but widens the propagation window. Is 5-minute propagation acceptable for normal entitlement changes, and does any support flow need a sub-minute revoke?
  • Pricing tiers. The free-tier threshold is defined; the paid price points and tier structure are not documented here. See the global Open questions.