ValGuide Docs
EngineeringArchitecture

Image Delivery

How ValGuide stores, transforms, and delivers images to visitors and curators.

System Overview

ValGuide stores all media (images, audio, video) in Cloudflare R2. Images are transformed on the fly — resized, format-converted, and quality-optimized — before reaching the browser. Audio and video are served directly from R2 without transformation.

The active image delivery provider is controlled by VITE_IMAGE_DELIVERY_PROVIDER when set, with fallback to the legacy VITE_IMAGE_PROVIDER. Supported providers are cloudflare, imagekit, and origin.

The schema-level fallback default remains cloudflare until deployment envs are switched explicitly.

Cloudflare mode (VITE_IMAGE_DELIVERY_PROVIDER=cloudflare)

┌─────────────┐     ┌──────────────────────┐     ┌───────────────┐     ┌─────────┐
│   R2 Bucket │────▶│  Image Guard Worker  │────▶│  Edge Cache   │────▶│ Browser │
│  (originals)│     │  /i/w-640/path.jpg   │     │  (Cloudflare) │     │  + SW   │
└─────────────┘     └──────────────────────┘     └───────────────┘     └─────────┘

                    fetch(origin, {
                      cf: { image: {
                        width, quality,
                        format, fit
                      }}
                    })

ImageKit mode (VITE_IMAGE_DELIVERY_PROVIDER=imagekit)

┌─────────────┐     ┌──────────────────────┐     ┌─────────┐
│   R2 Bucket │────▶│  ImageKit CDN        │────▶│ Browser │
│  (originals)│     │  (transforms + CDN)  │     │  + SW   │
└─────────────┘     └──────────────────────┘     └─────────┘

Architecture Layers

LayerWhat it doesDetails
R2Stores original uploaded filesEU jurisdiction, $0 egress via Cloudflare CDN
Image Guard WorkerValidates transform params, fetches origin with cf.image optionsRuns at assets.valguide.com/i/*
Cloudflare EdgePerforms the actual resize/encode as part of the Worker's fetch() callCaches each unique (source + params) combo
Browser + Service WorkerCaches transformed images locallyCache-Control: public, max-age=31536000, immutable

Request Flow

1. <Image> component generates /i/ URLs

The custom <Image> component (wrapping @unpic/react) uses a transformer that converts source URLs into /i/ URLs with transform params:

Source:  https://assets.valguide.com/orgs/abc/cover.jpg
srcset:  https://assets.valguide.com/i/w-320/orgs/abc/cover.jpg 320w,
         https://assets.valguide.com/i/w-640/orgs/abc/cover.jpg 640w,
         https://assets.valguide.com/i/w-960/orgs/abc/cover.jpg 960w,
         ...

2. Worker validates and transforms

The Image Guard Worker at /i/*:

  1. Parses the URL: /i/w-640,q-80/orgs/abc/cover.jpg
  2. Snaps width to the nearest allowed value: 320, 640, 960, 1280, 1920 (e.g., w-999960)
  3. Clamps quality to max 85 (default 75)
  4. Negotiates format from the browser's Accept header (AVIF → WebP → JPEG)
  5. Ignores unknown transform keys silently (forward-compatible)
  6. Fetches the original from R2 with cf.image options:
fetch('https://assets.valguide.com/orgs/abc/cover.jpg', {
  cf: {
    image: {
      width: 640,
      quality: 75,
      format: 'webp', // negotiated from Accept header
      fit: 'cover',
    },
  },
})

Cloudflare's edge performs the resize/encode as part of this fetch() call. The Worker never handles image bytes — it only validates and forwards.

3. Caching (three layers)

LayerBehaviorTTL
Cloudflare EdgeCaches each unique (source + params) combo globallyFollows origin Cache-Control (min 1 hour)
Service WorkermediaCacheFirst strategy for assets.valguide.comUntil SW update
BrowserCache-Control: immutable — no revalidation during max-age1 year

On repeat requests, the browser serves from local cache without any network call.

URL Format

https://assets.valguide.com/i/{transforms}/{storagePath}

Transform params

ParamRequiredExampleDescription
w-{n}Now-640Width in pixels. Snapped to nearest allowed: 320, 640, 960, 1280, 1920. Defaults to 1920 if missing.
h-{n}Noh-400Height in pixels
q-{n}Noq-80Quality 1–85 (default: 75)

Multiple params are comma-separated: /i/w-640,q-80/path.jpg

Fallback behavior

The Worker never returns 400 errors for transform issues — it uses sensible fallbacks instead:

ScenarioFallback
Width not in whitelist (w-999)Snapped to nearest allowed width (960)
Missing width (/i/q-80/path)Defaults to 1920 (largest breakpoint)
Unknown transform key (x-1)Silently ignored
Invalid value (w-abc, w-0)Param ignored; width falls back to 1920
Missing storage path404 (genuinely broken URL)

Always enforced (not configurable via URL)

OptionValueWhy
formatAuto-negotiated (AVIF > WebP > JPEG)Optimal format per browser
fitcoverFill the area, crop if needed

Examples

/i/w-320/orgs/abc/cover.jpg          → 320px wide, q75, auto format
/i/w-1920/orgs/abc/cover.jpg         → 1920px wide, q75, auto format
/i/w-640,q-80/orgs/abc/cover.jpg     → 640px wide, q80, auto format
/i/w-640,h-400/orgs/abc/cover.jpg    → 640×400, q75, auto format

Width Whitelist

All requested widths are snapped to the nearest of 5 allowed values: 320, 640, 960, 1280, 1920

The Worker never rejects invalid widths — instead it snaps w-999 to 960, w-50 to 320, etc. This ensures bots and crawlers always get a valid image while still capping unique transformations to 5 per source image.

This exists for three reasons:

  1. Cost control — Cloudflare bills per unique (source + params) transformation per month ($0.50/1,000). With 5 widths, 1,000 images = 5,000 unique transformations (within free tier). Without restriction, a bot could generate thousands of billable variants.

  2. Cache efficiency — Fewer variants means higher cache hit rates at the edge. 5 widths cover all standard responsive breakpoints.

  3. Predictability — Monthly cost scales linearly with image count: images × 5 = unique transformations.

Code Locations

ComponentFile
<Image> component + transformerpackages/core/ui/components/image.tsx
Image delivery provider contractpackages/core/platform/images/image-delivery.ts
URL helpers (getAssetImageUrl, etc.)packages/core/features/assets/image-url.ts
Image Guard Workerworkers/image-guard/src/index.ts
Worker configworkers/image-guard/wrangler.jsonc
Service worker cachingapps/app/public/sw.js
Asset base URL env varsVITE_ASSET_BASE_URL with fallback to VITE_R2_PUBLIC_URL in packages/core/platform/images/asset-base-url.ts

Using the <Image> Component

The custom Image component provides automatic responsive images, lazy loading, and content layout shift prevention.

// In apps (studio, app, admin):
import { Image } from '@valguide/ui/components/image'

// In packages/core:
import { Image } from '@valguide/core/ui/components/image'

Layout Modes

ModeWhen to UseExample
layout="fullWidth"Image fills its container width (most common)Cover images, cards, thumbnails
layout="constrained"Image has a max size but can shrinkFullscreen image viewer

For component patterns, code examples, and usage conventions, see Image Handling.

Pricing

We use Cloudflare Image Transformations (not the Cloudflare Images storage product).

TierIncludedOverage
Free5,000 unique transformations/monthNew transforms return error; cached ones keep working
Paid5,000 included+$0.50 per 1,000/month

A "unique transformation" = unique (source image + params) combo, billed once per calendar month. Repeat requests serve from cache at no cost. There are no delivery fees.

Image count× 5 widthsMonthly cost (Paid)
5002,500$0
1,0005,000$0
2,00010,000$2.50
5,00025,000$10.00
  • Image Handling<Image> component patterns, URL helpers, fallbacks, usage inventory, and coding conventions

On this page