Skip to content
New site — Virtual Media Folders now has its own home. Visit vmf.soderlind.no →

Architecture

This document describes the system as it is implemented. Decisions and their rationale are in adr/ (ADRs 0006–0011 are current; 0001–0005 are superseded). Domain terms such as Original, Variant, Plan and Presign are defined in GLOSSARY.md.

Tachyon Edge is a single Cloudflare Worker that serves image URLs generated by the Tachyon WordPress plugin. For each request it reads an Original from R2 or an HTTP Origin, computes the output geometry that Human Made Tachyon v3.0.8 would produce, and asks the Cloudflare Images binding to apply that geometry and encode the Variant.

Included:

  • The Plugin Contract: URL shape, query arguments, output dimensions, output format, status codes and cache headers (ADR 0006).
  • Reading Originals from an R2 binding or an HTTP base URL (ADR 0008).
  • Verifying SigV4 presigns for private prefixes (ADR 0009).
  • Caching unsigned public Variants with the Cache API (ADR 0010).

Not included:

  • Byte-identical output with Tachyon. Pixels, file sizes, metadata and PNG palette differ; see the Deviations in ADR 0006.
  • Uploading or storing Originals. The Worker only reads them.
  • Content negotiation. X-WebP must be set by something in front of the Worker.
  • Persistent state of its own. The Worker has no database, Durable Object or KV namespace.
ComponentResponsibilityImplementationDepends on
Worker entryOrchestrates a request: authorize, cache lookup, fetch, transform or pass through, cache write, error mappingsrc/worker.ts (handleRequest, serve)every module below
Contract parserTurns a request into a TachyonRequest: Object Key, validated arguments, WebP choice, Presignsrc/contract.ts (parseRequest, objectKey, validateArgs, lambdaQuery)none
Presign verifierRecomputes the S3 SigV4 query signature and computes the remaining validitysrc/sigv4.ts (verifyPresign, secondsUntilExpiry)contract.ts types, WebCrypto
OriginsRead an Original by storage key from R2 or HTTP; report a stable Origin identity for cache keyssrc/origin.ts (fetchOriginal, storageKey, originIdentity, httpOriginUrl)bindings.ts Bucket, fetch
Header probeRead format, stored size and EXIF orientation from the header of JPEG, PNG, GIF and WebP (AVIF: format only); map them to a Sourcesrc/probe.ts (probe, toSource)geometry.ts types
Geometry plannerCompute Tachyon v3.0.8’s output size, format and quality, and express them as explicit binding transformssrc/geometry.ts (plan, orientationTransforms, zoomQuality)contract.ts types
Binding interfacesMinimal structural types for IMAGES, BUCKET and the execution context, so tests can supply fakessrc/bindings.ts, checked against @cloudflare/workers-types by src/bindings-check.tsnone

Dependency direction: worker.ts depends on all other modules. contract.ts depends on nothing. geometry.ts, probe.ts, origin.ts and sigv4.ts depend only on types from contract.ts, geometry.ts or bindings.ts. Keep geometry.ts free of I/O and bindings: the oracle tests run it in Node.

Cloudflare resources, configured in wrangler.jsonc:

Binding or variableUsed byPurpose
IMAGESworker.tsImages binding: info for sizing formats the probe cannot size, and input().transform().output()
BUCKETorigin.tsR2 bucket holding Originals when TACHYON_ORIGIN=r2
TACHYON_* vars and the TACHYON_SIGNING_SECRET_ACCESS_KEY secretworker.ts, origin.tsSee the configuration table in README.md
build.commandWranglerRuns scripts/check-config.mjs before every deploy and wrangler dev, so a TACHYON_R2_BUCKET that differs from bucket_name fails the deploy

Representative flow: GET /uploads/2026/11/photo.jpg?resize=300,200

Section titled “Representative flow: GET /uploads/2026/11/photo.jpg?resize=300,200”
  1. handleRequest (src/worker.ts) calls serve and turns any thrown HttpError or unexpected error into a response.
  2. parseRequest (src/contract.ts) joins duplicate query parameters with commas (lambdaQuery), drops arguments that fail Tachyon’s validation patterns (validateArgs), derives the Object Key uploads/2026/11/photo.jpg (objectKey), selects WebP from webp= or the X-WebP header, and collects a Presign from presign= or raw X-Amz-* parameters. A malformed percent-escape throws and becomes a 500, as in Tachyon.
  3. authorize (src/worker.ts) checks whether the storage key (TACHYON_KEY_PREFIX + Object Key) starts with a private prefix. A private key needs signing credentials and a Presign that has not expired and verifies with verifyPresign; otherwise the result is 404. An unsigned public request gets max-age=31536000. A presigned request gets the remaining validity as max-age.
  4. If the request is a GET, unsigned and public, and the Origin can be identified (cacheableOrigin: the R2 Origin needs TACHYON_R2_BUCKET), cacheKey adds a __tachyon_variant parameter to the request URL. Its value is the SHA-256 of PROCESSING_VERSION, originIdentity and the WebP choice. On a hit the cached Response is returned with a fresh Last-Modified.
  5. fetchOriginal (src/origin.ts) reads the storage key from R2, or from TACHYON_HTTP_ORIGIN with each path segment encoded and ./.. segments rejected. A missing object, or an HTTP 403 or 404, becomes 404. Other failures become 502.
  6. Size gate: an Original whose known size exceeds MAX_INPUT_BYTES (20 MB) is passed through unmodified (passthrough). An Original of unknown length is buffered by readAtMost up to the limit; if it is longer, the buffered prefix plus the rest of the stream is passed through.
  7. describe (src/worker.ts) probes the header (probe). If the probe finds the format but not the size (AVIF), it falls back to IMAGES.info. toSource swaps width and height for EXIF orientations 5–8, so the planner works in display coordinates.
  8. plan (src/geometry.ts) emits trim for crop, then squeeze or cover steps for resize, fit, w/h, then border for lb. It returns the output format and quality. PlanError becomes a 500.
  9. transform (src/worker.ts) prepends orientationTransforms (explicit rotate/flip for PNG and WebP; the binding auto-orients JPEG itself), applies each step through the binding and calls output. anim is set only for GIF and WebP output, because the binding rejects it otherwise (error 9401). PNG output carries no quality, because the binding ignores it. If the binding rejects an animation whose frames total 100 megapixels or more (error 9413, ANIMATION_TOO_LARGE), the Original is passed through unmodified.
  10. The Response carries Content-Type, Cache-Control: max-age=… and Last-Modified. For a cacheable request, context.waitUntil stores a clone in the Cache API, so the cache write doesn’t delay the response.
FailureStatus and headersWhere
Malformed path escape500 Internal Server Error, Cache-Control: no-storeserve catching parseRequest
Private key without credentials, without Presign, expired, or failing verification404 File not found. (text/html)authorize
Expired or invalid Presign on a public key404 (expiry always applies; the signature is checked only when credentials are configured)authorize
Original missing, or HTTP Origin 403/404404 File not found.fetchOriginal → serve
R2 read error, HTTP Origin error, misconfigured Origin502 Bad Gateway, no-storefetchOriginal → serve
Unsupported or unsizable Original, PlanError, binding error other than 9413500, no-store, reason logged to Workers Logsdescribe, transform
Cache read or write failureLogged; the request proceeds uncachedserve
Original over 20 MB200 with the unmodified bytes and a Content-Type from the file extensionpassthrough
Animation of 100 megapixels or more (binding error 9413)200 with the unmodified bytes and a Content-Type from the file extensiontransform → passthrough
R2 Origin without TACHYON_R2_BUCKETServed uncached; configuration error loggedcacheableOrigin

The Worker does not retry anything. Error responses are never written to the cache, because serve throws before cache.put.

  • Trust boundary: the query string, path and X-WebP header are untrusted. validateArgs drops values that fail Tachyon’s patterns before they reach the planner. httpOriginUrl stops keys from escaping the HTTP Origin base path. Presigns are trusted only after verifyPresign, which accepts only AWS4-HMAC-SHA256 with SignedHeaders=host and no session token, rejects expiries over 7 days and dates more than 15 minutes in the future, and compares signatures in constant time.
  • Signed key vs storage key: WordPress signs the Object Key without TACHYON_KEY_PREFIX, so verifyPresign uses TachyonRequest.key. Private prefixes match the storage key (storageKey), so a prefix can’t be bypassed by changing TACHYON_KEY_PREFIX.
  • Cache boundary: only unsigned GET requests for non-private keys use the Cache API. Presigned and private Variants are never cached by the Worker.
  • Resource limits: MAX_INPUT_BYTES is the largest Original the Worker will transform; larger Originals are streamed through untransformed. It is not a strict memory bound: readAtMost appends a whole chunk before checking the limit, so it can buffer up to one chunk beyond MAX_INPUT_BYTES before switching to passthrough.
  • State: each request is independent. The only shared state is the Cache API, and its entries are immutable apart from replacement under the same key.
RuleEnforced byVerified by
Output dimensions match Tachyon v3.0.8 for every argument combinationplan, orientationTransforms, toSourcetests/test-geometry-oracle.ts (oracle in tests/oracle/tachyon-v3.0.8.ts; do not edit it), tests/test-worker.ts, tests/remote/smoke-remote.ts
Arguments are validated exactly like Tachyon, including Release QuirksvalidateArgs, lambdaQuerytests/test-contract.ts
Private storage keys are served only with a verified, unexpired Presign; missing credentials fail closedisPrivate, authorize, verifyPresigntests/test-worker.ts (“private prefixes and presigns”), tests/test-sigv4.ts
A cached Variant is reused only for the same URL, processing behavior, Origin and WebP choicecacheKey, originIdentity, PROCESSING_VERSIONtests/test-worker.ts (“cache”)
TACHYON_R2_BUCKET equals the bucket_name bound as BUCKETscripts/check-config.mjs as the Wrangler build command (checks wrangler.jsonc, not --var overrides); cacheableOrigin skips the cache when it is emptytests/test-config.ts, tests/test-worker.ts (“cache”)
Errors are never stored in the Worker cache; 5xx responses also forbid browser caching (404s carry no cache directive)serve (no cache.put on throw), handleRequest (no-store on 5xx only)tests/test-worker.ts (“cache”)
The structural binding interfaces match the real Cloudflare typessrc/bindings-check.tsnpm run check:worker
The fake binding mirrors verified binding behavior (JPEG-only auto-orientation, 9401 for anim, 9413 for animations of 100 megapixels or more, truecolor PNG)tests/helpers/fake-images.tsnpm run test:remote against a deployed Worker
ChangePrimary locationAlso update
Support a new query argumentARG_NAMES and validateArgs in src/contract.ts, then plan in src/geometry.tstests/test-contract.ts, oracle cases in tests/test-geometry-oracle.ts, using.md
Change output for an unchanged URL (encoder, quality, orientation)transform in src/worker.ts or planBump PROCESSING_VERSION, tests/helpers/fake-images.ts if binding behavior is involved, the remote smoke suite, ADR 0006 Deviations
Add an Origin typefetchOriginal and originIdentity in src/origin.tsEnv in src/worker.ts, wrangler.jsonc, README configuration, tests/test-worker.ts, ADR 0008
Change private-image rulesisPrivate and authorize in src/worker.ts, verifyPresign in src/sigv4.tstests/test-sigv4.ts, tests/test-worker.ts, ADR 0009
Support a new input formatprobe in src/probe.ts, SourceFormat and plan format selection in src/geometry.tsEXTENSION_TYPES in src/worker.ts, oracle fixtures, remote smoke fixtures
Use a new binding featureTransform in src/geometry.ts or src/bindings.tsnpm run check:worker, tests/helpers/fake-images.ts
LayerCommandWhat it proves
Geometry oraclenpm testThe planner matches Tachyon v3.0.8’s own Sharp pipeline on fixtures and random sizes
Contract, SigV4 and Workernpm testParsing, presign verification and end-to-end Worker behavior, using a Sharp-backed fake binding
Configurationnpm test, and the Wrangler build command on deployTACHYON_R2_BUCKET matches the bound bucket in wrangler.jsonc
Type checknpm run check:workerSource and binding interfaces against @cloudflare/workers-types
Remote smokenpm run test:remote (see README.md)Geometry and binding semantics on a deployed Worker with the real Images binding, including AVIF input and the animation limit
WP-CLI acceptancenpm run test:acceptance (see acceptance-test.md)URLs that tachyon-plugin 0.11.1 generates in WordPress (image src, srcset and the_content) return the declared dimensions, type and cache header from a deployed Worker

The local fake binding is only as accurate as the remote smoke suite that checks it. When the binding’s behavior is in doubt, run the remote suite (ADR 0011).

Platform behavior that differs from Cloudflare’s documentation

Section titled “Platform behavior that differs from Cloudflare’s documentation”

Measured on the deployed Worker in November 2026 and covered by the remote smoke suite (ADR 0007):

  • AVIF Originals are documented as Enterprise-only, but the binding decoded them on a non-Enterprise account. If an account rejects them, AVIF Originals return a 500; set TACHYON_SMOKE_SKIP_AVIF=1 for the smoke suite.
  • Animations over 50 megapixels are documented as returned untransformed. The binding transforms them up to 100 megapixels and fails with error 9413 above that; the Worker then serves the Original unmodified.