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.
Behavior
Section titled “Behavior”Earning clean images
Section titled “Earning clean images”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.
How a request is matched
Section titled “How a request is matched”For each request the Worker checks the allow-list in this order and stops at the first match:
- API key — from the
X-Vumbnail-Keyheader or?key=query param (keys look likevb_live_<label>_<random>). - Client IP — from
CF-Connecting-IP. - Referer host — lowercased,
www./m.prefixes ignored. - User-agent substring — for mobile apps that can’t set a referrer.
Any match ⇒ variant = clean.
Keeping the allow-list current
Section titled “Keeping the allow-list current”The allow-list has two read paths and one write path:
- Write: the
accountsWorker (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 committedcustomers.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.jsonbundled 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 & outputs
Section titled “Inputs & outputs”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.
States & edge cases
Section titled “States & edge cases”- 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.
Data shape
Section titled “Data shape”// 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 KVtype 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).
Open questions
Section titled “Open questions”- 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.