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.
Purpose
Section titled “Purpose”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-WebPmust be set by something in front of the Worker. - Persistent state of its own. The Worker has no database, Durable Object or KV namespace.
Major components
Section titled “Major components”| Component | Responsibility | Implementation | Depends on |
|---|---|---|---|
| Worker entry | Orchestrates a request: authorize, cache lookup, fetch, transform or pass through, cache write, error mapping | src/worker.ts (handleRequest, serve) | every module below |
| Contract parser | Turns a request into a TachyonRequest: Object Key, validated arguments, WebP choice, Presign | src/contract.ts (parseRequest, objectKey, validateArgs, lambdaQuery) | none |
| Presign verifier | Recomputes the S3 SigV4 query signature and computes the remaining validity | src/sigv4.ts (verifyPresign, secondsUntilExpiry) | contract.ts types, WebCrypto |
| Origins | Read an Original by storage key from R2 or HTTP; report a stable Origin identity for cache keys | src/origin.ts (fetchOriginal, storageKey, originIdentity, httpOriginUrl) | bindings.ts Bucket, fetch |
| Header probe | Read format, stored size and EXIF orientation from the header of JPEG, PNG, GIF and WebP (AVIF: format only); map them to a Source | src/probe.ts (probe, toSource) | geometry.ts types |
| Geometry planner | Compute Tachyon v3.0.8’s output size, format and quality, and express them as explicit binding transforms | src/geometry.ts (plan, orientationTransforms, zoomQuality) | contract.ts types |
| Binding interfaces | Minimal structural types for IMAGES, BUCKET and the execution context, so tests can supply fakes | src/bindings.ts, checked against @cloudflare/workers-types by src/bindings-check.ts | none |
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 variable | Used by | Purpose |
|---|---|---|
IMAGES | worker.ts | Images binding: info for sizing formats the probe cannot size, and input().transform().output() |
BUCKET | origin.ts | R2 bucket holding Originals when TACHYON_ORIGIN=r2 |
TACHYON_* vars and the TACHYON_SIGNING_SECRET_ACCESS_KEY secret | worker.ts, origin.ts | See the configuration table in README.md |
build.command | Wrangler | Runs 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”handleRequest(src/worker.ts) callsserveand turns any thrownHttpErroror unexpected error into a response.parseRequest(src/contract.ts) joins duplicate query parameters with commas (lambdaQuery), drops arguments that fail Tachyon’s validation patterns (validateArgs), derives the Object Keyuploads/2026/11/photo.jpg(objectKey), selects WebP fromwebp=or theX-WebPheader, and collects a Presign frompresign=or rawX-Amz-*parameters. A malformed percent-escape throws and becomes a 500, as in Tachyon.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 withverifyPresign; otherwise the result is 404. An unsigned public request getsmax-age=31536000. A presigned request gets the remaining validity asmax-age.- If the request is a GET, unsigned and public, and the Origin can be identified (
cacheableOrigin: the R2 Origin needsTACHYON_R2_BUCKET),cacheKeyadds a__tachyon_variantparameter to the request URL. Its value is the SHA-256 ofPROCESSING_VERSION,originIdentityand the WebP choice. On a hit the cached Response is returned with a freshLast-Modified. fetchOriginal(src/origin.ts) reads the storage key from R2, or fromTACHYON_HTTP_ORIGINwith each path segment encoded and./..segments rejected. A missing object, or an HTTP 403 or 404, becomes 404. Other failures become 502.- 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 byreadAtMostup to the limit; if it is longer, the buffered prefix plus the rest of the stream is passed through. describe(src/worker.ts) probes the header (probe). If the probe finds the format but not the size (AVIF), it falls back toIMAGES.info.toSourceswaps width and height for EXIF orientations 5–8, so the planner works in display coordinates.plan(src/geometry.ts) emitstrimforcrop, thensqueezeorcoversteps forresize,fit,w/h, thenborderforlb. It returns the output format and quality.PlanErrorbecomes a 500.transform(src/worker.ts) prependsorientationTransforms(explicitrotate/flipfor PNG and WebP; the binding auto-orients JPEG itself), applies each step through the binding and callsoutput.animis set only for GIF and WebP output, because the binding rejects it otherwise (error 9401). PNG output carries noquality, 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.- The Response carries
Content-Type,Cache-Control: max-age=…andLast-Modified. For a cacheable request,context.waitUntilstores a clone in the Cache API, so the cache write doesn’t delay the response.
Failure behavior
Section titled “Failure behavior”| Failure | Status and headers | Where |
|---|---|---|
| Malformed path escape | 500 Internal Server Error, Cache-Control: no-store | serve catching parseRequest |
| Private key without credentials, without Presign, expired, or failing verification | 404 File not found. (text/html) | authorize |
| Expired or invalid Presign on a public key | 404 (expiry always applies; the signature is checked only when credentials are configured) | authorize |
| Original missing, or HTTP Origin 403/404 | 404 File not found. | fetchOriginal → serve |
| R2 read error, HTTP Origin error, misconfigured Origin | 502 Bad Gateway, no-store | fetchOriginal → serve |
Unsupported or unsizable Original, PlanError, binding error other than 9413 | 500, no-store, reason logged to Workers Logs | describe, transform |
| Cache read or write failure | Logged; the request proceeds uncached | serve |
| Original over 20 MB | 200 with the unmodified bytes and a Content-Type from the file extension | passthrough |
| Animation of 100 megapixels or more (binding error 9413) | 200 with the unmodified bytes and a Content-Type from the file extension | transform → passthrough |
R2 Origin without TACHYON_R2_BUCKET | Served uncached; configuration error logged | cacheableOrigin |
The Worker does not retry anything. Error responses are never written to the cache, because serve throws before cache.put.
Boundaries
Section titled “Boundaries”- Trust boundary: the query string, path and
X-WebPheader are untrusted.validateArgsdrops values that fail Tachyon’s patterns before they reach the planner.httpOriginUrlstops keys from escaping the HTTP Origin base path. Presigns are trusted only afterverifyPresign, which accepts onlyAWS4-HMAC-SHA256withSignedHeaders=hostand 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, soverifyPresignusesTachyonRequest.key. Private prefixes match the storage key (storageKey), so a prefix can’t be bypassed by changingTACHYON_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_BYTESis the largest Original the Worker will transform; larger Originals are streamed through untransformed. It is not a strict memory bound:readAtMostappends a whole chunk before checking the limit, so it can buffer up to one chunk beyondMAX_INPUT_BYTESbefore 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.
Invariants
Section titled “Invariants”| Rule | Enforced by | Verified by |
|---|---|---|
| Output dimensions match Tachyon v3.0.8 for every argument combination | plan, orientationTransforms, toSource | tests/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 Quirks | validateArgs, lambdaQuery | tests/test-contract.ts |
| Private storage keys are served only with a verified, unexpired Presign; missing credentials fail closed | isPrivate, authorize, verifyPresign | tests/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 choice | cacheKey, originIdentity, PROCESSING_VERSION | tests/test-worker.ts (“cache”) |
TACHYON_R2_BUCKET equals the bucket_name bound as BUCKET | scripts/check-config.mjs as the Wrangler build command (checks wrangler.jsonc, not --var overrides); cacheableOrigin skips the cache when it is empty | tests/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 types | src/bindings-check.ts | npm 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.ts | npm run test:remote against a deployed Worker |
Where to make changes
Section titled “Where to make changes”| Change | Primary location | Also update |
|---|---|---|
| Support a new query argument | ARG_NAMES and validateArgs in src/contract.ts, then plan in src/geometry.ts | tests/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 plan | Bump PROCESSING_VERSION, tests/helpers/fake-images.ts if binding behavior is involved, the remote smoke suite, ADR 0006 Deviations |
| Add an Origin type | fetchOriginal and originIdentity in src/origin.ts | Env in src/worker.ts, wrangler.jsonc, README configuration, tests/test-worker.ts, ADR 0008 |
| Change private-image rules | isPrivate and authorize in src/worker.ts, verifyPresign in src/sigv4.ts | tests/test-sigv4.ts, tests/test-worker.ts, ADR 0009 |
| Support a new input format | probe in src/probe.ts, SourceFormat and plan format selection in src/geometry.ts | EXTENSION_TYPES in src/worker.ts, oracle fixtures, remote smoke fixtures |
| Use a new binding feature | Transform in src/geometry.ts or src/bindings.ts | npm run check:worker, tests/helpers/fake-images.ts |
Testing layers
Section titled “Testing layers”| Layer | Command | What it proves |
|---|---|---|
| Geometry oracle | npm test | The planner matches Tachyon v3.0.8’s own Sharp pipeline on fixtures and random sizes |
| Contract, SigV4 and Worker | npm test | Parsing, presign verification and end-to-end Worker behavior, using a Sharp-backed fake binding |
| Configuration | npm test, and the Wrangler build command on deploy | TACHYON_R2_BUCKET matches the bound bucket in wrangler.jsonc |
| Type check | npm run check:worker | Source and binding interfaces against @cloudflare/workers-types |
| Remote smoke | npm 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 acceptance | npm 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=1for 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.
📦 Source: soderlind/tachyon-edge · Edit on GitHub