Skip to content

Edge watermark router

The watermark router is a tiny Cloudflare Snippet that runs ahead of the thumbnail Worker. It is the deterministic watermark control point: a watermark is never written into the bare URL’s cache — it is a 307 redirect to a distinct wm.jpg key, so the bare URL only ever holds clean bytes. Flipping a host onto watermarks is additive: add the host to WM_DOMAINS and pin its current ids into the hot set. Because the redirect also skips the metered Worker (and its KV reads), every flip is a cost saving, not a cost. Everything else passes through untouched.

The Snippet evaluates a shallow decision tree (it has a ~5 ms CPU budget, so no heavy logic):

  1. Paying customer? If the client IP or referrer domain is in the paying set, pass through to the Worker (which serves a clean image). Paying customers are never short-circuited.
  2. Malformed path /.jpg? 308 redirect to the shared error.jpg.
  3. Borås WordPress loop? If the exact loop predicate matches (video ID, UA, self-referrer, ASN 14061), 307 to that video’s wm.jpg. See Redirects.
  4. Hot-set short-circuit? If the request is GET/HEAD for a hot video ID from a whitelisted watermark referrer (and not a paying domain/IP), 307 to https://cache.vumbnail.com/thumbnails/<videoId>/wm.jpg.
  5. Otherwise pass through to the Worker.

The router never rewrites bytes. Watermarking a request means answering it with a 307 to a distinct wm.jpg cache key — the bare thumbnail URL is never asked to hold watermarked bytes, so no shared edge cache can pin wm where a clean image belongs. This is what makes the mechanism deterministic: the decision is re-evaluated per request at the edge instead of being frozen into a referrer-blind cache entry, and a host flip (or unflip) takes effect as fast as the Snippet deploy propagates.

The hot set is a performance pin, not a policy boundary

Section titled “The hot set is a performance pin, not a policy boundary”

Cold ids from a flipped host still pass through to the Worker, which applies the same referrer-cohort decision on its miss path — the same watermark outcome, just metered and cache-dependent. The hot set only decides where the decision is served (a deterministic, unmetered redirect at the edge), not whether the host is watermarked.

Snippets run before Workers in Cloudflare’s pipeline and are far cheaper per request. The hot set is a hand-maintained list of the most-requested watermark-eligible video IDs whose wm.jpg is pre-generated in R2. Serving those via a redirect skips a metered Worker invocation and its KV reads. At current volume the blended saving is on the order of $1.07 per million requests diverted; the Snippet pays for itself immediately.

Because cache.vumbnail.com is a public R2 domain, a WAF rule blocks direct fetches of any …/clean.jpg path, so the public bucket can only ever serve the watermarked or shared objects.

Inputs: request method, path, Referer, CF-Connecting-IP / True-Client-IP / X-Forwarded-For, User-Agent, and Cloudflare ASN.

Outputs: a 307/308 redirect to a public R2 object, or pass-through to the thumbnail Worker. The Snippet itself never renders or fetches an image.

  • Hot ID without a pre-rendered wm.jpg — the verify-hotset step ensures every hot ID has its watermarked object in R2 before deploy; a missing object would redirect to a 404, so the hot set and R2 are kept in sync.
  • Hot ID ages out of the set — the request falls through to the Worker, which makes the same cohort decision on miss. Coverage holds; determinism (and the cost saving) is what decays. Rotating-catalog hosts decay fastest — see hot-set-management.
  • Dev/local referrer — dev and local traffic is served clean by policy, detected by referrer host (localhost, 127.0.0.1, [::1], *.local, *.test, RFC-1918 ranges, tunnel hosts) — never by user-agent. The explicit early return (isDevCleanReferrer) is Planned; today these referrers simply never match WM_DOMAINS and pass through.
  • Size / bundle limits — the deployed Snippet must stay within a 32 KB bundle and 5 ms CPU; it uses plain Set lookups and avoids regex. Current usage is ~10.5 KB.
  • Paying customer on a watermark referrer — the paying check runs first, so they pass through to a clean image even if the referrer is whitelisted.
  • Rollback — deploys take a snapshot first; rollback:snapshot restores the previous Snippet and rule. Restoring clean delivery for a flipped host is the dual-lane procedure above, not a Snippet rollback alone — see reverting-deploys.
  • Deploy / verify / smokebun --filter @repo/snippet deploy, verify-hotset, and smoke are the operational commands; lint enforces the size budget.
// Hand-maintained constants in watermark-router.snippet.js
const HOT_VIDEO_IDS: Set<string> // ~68 most-requested watermark-eligible ids
const WM_DOMAINS: Set<string> // referrers that trigger watermarking:
// the always-on self-referred hosts
// S01–S04, plus flipped rollout hosts
// (H01, …) — a flip is additive
const PAYING_DOMAINS: Set<string> // e.g. C01's domain → always pass through
const PAYING_IPS: Set<string> // e.g. C01's entitled IP → always pass through
const BORAS_WORDPRESS_VIDEO_IDS: Set<string> // the two pinned loop ids
const BORAS_ASN = 14061
type SnippetOutcome =
| { action: "passthrough" }
| { action: "redirect"; status: 307 | 308; location: string }
  • How far to expand the hot set. Larger hot sets (e.g. a top200-per-host slice) save more Worker/KV spend but grow the bundle toward the 32 KB ceiling (~22.5 KB for the largest candidate). The coverage-vs-size trade-off is open, and the hot-set refresh automation is not built — rotating-catalog hosts decay toward ~0% hot coverage in ~8 weeks without it. See hot-set-management.
  • Cache-Control: no-store on the 307 — required before flipping any host above ~1M requests/month, so intermediaries cannot pin the redirect itself.
  • isDevCleanReferrer early return — the explicit dev/local clean short-circuit is Planned, not built.
  • No-referrer policy at the edge — no-referrer → clean is decided; the written sign-off document is still open. See Freemium watermarking.
  • Keeping PAYING_DOMAINS/PAYING_IPS in sync with the Worker’s KV entitlement projection (the Snippet list is currently hand-maintained). Tracked on the global Open questions.