Image Transformations
Image transformations let you serve optimized versions of your images automatically. Instead of manually creating multiple sizes, Pixflare generates them on-demand and caches them at the edge.
Why transformations matter
Serving the right image size saves bandwidth, speeds up your site, and improves user experience. A mobile device doesn't need a 4K desktop image. Transformations handle this automatically:
- Bandwidth savings: A 512px image is 80-90% smaller than a 2048px original
- Faster page loads: Smaller images mean quicker downloads
- Modern formats: Automatic conversion to AVIF or WebP based on browser support
- No manual work: Request any size via URL, generated on first use
- Edge caching: Transformed images cached globally for instant delivery
Default Presets
Pixflare includes 9 built-in presets covering common use cases:
| Preset | Dimensions | Fit Mode | Description |
|---|---|---|---|
w128 | Max width 128px | scale-down | Tiny thumbnails |
w256 | Max width 256px | scale-down | Small previews |
w512 | Max width 512px | scale-down | Mobile screens |
w1024 | Max width 1024px | scale-down | Default, tablets and small desktops |
w1536 | Max width 1536px | scale-down | Large desktops |
w2048 | Max width 2048px | scale-down | Retina displays |
thumb | 128x128px square | cover | Profile pictures, grid thumbnails |
og-image | 1200x630px | cover | Social media cards (Twitter, Facebook, etc) |
original | No transformation | - | Serve the unmodified uploaded image |
All width-based presets use scale-down fit mode, which means they shrink images to fit but never enlarge them. Aspect ratio is always preserved.
Default transform options
Every preset uses these defaults unless overridden:
- Quality: 85 (on a 1-100 scale)
- Format: Auto-negotiated (AVIF for modern browsers, WebP for older ones, original as fallback)
- Metadata: Stripped (removes EXIF data for privacy and smaller file sizes)
- Fit: scale-down (never enlarges images)
You can see the full preset configuration in packages/config/src/image-variants.ts.
Using Transformations
To request a transformed image, append the preset name to the URL:
# Original image
https://cdn.yourdomain.com/alice/vacation/beach.jpg
# 512px width version
https://cdn.yourdomain.com/alice/vacation/beach.jpg/w512
# Square thumbnail
https://cdn.yourdomain.com/alice/vacation/beach.jpg/thumb
# Social media card
https://cdn.yourdomain.com/alice/vacation/beach.jpg/og-imageThe same works with short IDs:
# Original
https://cdn.yourdomain.com/i/abc123xyz
# Transformed
https://cdn.yourdomain.com/i/abc123xyz/w1024What happens on first request
When you request a variant that hasn't been generated yet:
- Pixflare serves it on-demand using Cloudflare Image Resizing
- The transformed image is returned immediately
- A background job queues up to generate and store the variant in R2
- Future requests serve the pre-generated version from R2 (even faster)
If no preset is specified, the default preset (w1024) is served.
Format negotiation
Pixflare automatically serves the best format your browser supports:
- Modern browsers (Chrome 85+, Edge 91+, Safari 16+): AVIF (best compression)
- Older browsers with WebP support: WebP (good compression)
- Legacy browsers: Original format (JPG, PNG, etc)
This happens transparently based on the Accept header. You don't need to do anything.
SVG handling
SVG images are never transformed. They're vector graphics and scale infinitely, so transformations don't make sense. Pixflare always serves the original SVG regardless of the preset requested.
Adding/Editing Presets
You can control which presets are available and add custom ones. There are two configuration methods: environment variables (simple) and code-based (full control).
Method 1: Environment variables
The quickest way to restrict or enable presets is via environment variables in your wrangler.toml or .env file.
ALLOWED_VARIANTS controls which presets users can request:
[vars]
ALLOWED_VARIANTS = '["w128","w256","w512","w1024","w1536","w2048","thumb","og-image"]'If someone requests a preset not in this list, they get a 400 error. This is useful if you want to disable expensive presets like w2048.
DEFAULT_VARIANT sets the preset served when none is specified:
[vars]
DEFAULT_VARIANT = "w1024"R2_CUSTOM_DOMAIN is required for transformations to work:
[vars]
R2_CUSTOM_DOMAIN = "r2.yourdomain.com"Without this, Pixflare serves original images instead of transformed ones. See the Requirements section below for setup.
Method 2: Code-based configuration
For full control, edit the preset definitions directly. This lets you add custom presets with specific dimensions and transform options.
Step 1: Edit the presets file
Open packages/config/src/image-variants.ts.
Add your preset to the VARIANT_PRESETS array:
export const VARIANT_PRESETS = [
'original',
'w128',
'w256',
// ... existing presets
'w3072', // Your new preset
] as const;Then add the configuration to VARIANT_CONFIG:
export const VARIANT_CONFIG: Record<VariantPreset, VariantConfig> = {
// ... existing configs
w3072: {
description: 'Max width 3072px for 4K displays',
label: 'XXX-Large - 3072px',
transform: {
...DEFAULT_TRANSFORM_OPTIONS,
width: 3072,
fit: 'scale-down',
},
},
};Step 2: Update environment variables
Add your new preset to ALLOWED_VARIANTS:
[vars]
ALLOWED_VARIANTS = '["w128","w256","w512","w1024","w1536","w2048","w3072","thumb","og-image"]'Step 3: Rebuild and deploy
# Rebuild the config package
cd packages/config
pnpm build
# Rebuild the API
cd ../api
pnpm build
# Deploy
pnpm deploy:prodStep 4: Test
curl https://cdn.yourdomain.com/owner/album/image.jpg/w3072You can create presets with different fit modes, quality settings, or other options. See the Advanced Configuration section for all available options.
How it Works
This section explains the technical implementation of image transformations, including the request flow, caching strategy, and background generation.
Request flow
sequenceDiagram
participant Client
participant Gateway as Gateway Worker
participant API as API Worker
participant D1 as D1 Database
participant R2 as R2 Storage
participant CF as Cloudflare Image Resizing
participant Queue as Queue (Background)
Client->>Gateway: GET /alice/vacation/beach.jpg/w512
Gateway->>API: Route to CDN handler
API->>D1: Check image exists, privacy, deleted
alt Image not found or deleted
D1-->>API: Not found
API-->>Client: 404 or 410
end
API->>R2: Try fetch pre-generated variant
alt Variant exists in R2
R2-->>API: Return variant
API-->>Client: 200 with transformed image<br/>(Cache-Control: immutable, 1 year)
else Variant not generated yet
R2-->>API: Not found
API->>R2: Fetch original image
R2-->>API: Original image data
API->>CF: Transform on-demand<br/>(via cf.image fetch option)
CF-->>API: Transformed image
API->>Queue: Enqueue background generation
API-->>Client: 200 with transformed image
Queue-->>R2: Store variant for next time
endThe flow is optimized for speed. Pre-generated variants are served directly from R2. New variants are transformed on-demand and cached, then persisted in the background.
Implementation details
The CDN request handler lives in packages/api/src/routes/cdn.ts (lines 345-616).
1. Security checks
Before serving any image, Pixflare validates:
- Image exists in the database
- Not marked as deleted (returns 410 Gone if deleted)
- Privacy settings (private images require authentication)
- Upload completed (not just a created stub)
These checks are atomic to prevent time-of-check-time-of-use (TOCTOU) vulnerabilities.
2. Variant lookup
Pixflare looks for the pre-generated variant in R2:
const variantKey = getVariantKey(owner, album, filename, variant);
// Example: alice/vacation/beach.jpg/__variants/w512.jpg
const variantObject = await getObject(bucket, variantKey);If found, it's served immediately with aggressive caching headers:
'Cache-Control': 'public, max-age=31536000, immutable'This tells Cloudflare and browsers to cache the image for a year. Images are immutable (URLs never change), so this is safe.
3. On-demand transformation
If the variant doesn't exist, Pixflare generates it on the fly using Cloudflare Image Resizing. This requires the R2_CUSTOM_DOMAIN to be configured.
The transformation happens via Cloudflare's cf.image fetch option:
const r2ImageUrl = `https://${r2CustomDomain}/${originalKey}`;
const cfOptions = buildCfImageOptions(variant);
const transformedResponse = await fetch(r2ImageUrl, {
cf: cfOptions as RequestInitCfProperties
});You can see the buildCfImageOptions function in packages/api/src/lib/variants.ts.
Cloudflare does the heavy lifting:
- Fetches the original from R2
- Applies the transformation (resize, format conversion, etc)
- Returns a
cf-resizedheader if successful - Handles format negotiation based on the
Acceptheader
4. Background generation
After serving the on-demand transformation, Pixflare enqueues a background job to persist the variant in R2. This makes future requests faster (no transformation needed).
The queue consumer lives in packages/api/src/queue/variant-consumer.ts.
Note: The queue consumer currently has a placeholder for actual transformation logic (lines 147-154). On-demand transformation works perfectly, but background persistence needs implementation. For now, it stores the original image data.
Storage structure
Pixflare stores images and variants in R2 with this structure:
# Original images
owner/album/filename.jpg
# Variants
owner/album/filename/__variants/w512.jpg
owner/album/filename/__variants/thumb.jpg
owner/album/filename/__variants/og-image.jpgThis keeps variants organized under the original image's path. You can see the key-building functions in packages/shared/src/urls.ts.
Caching architecture
Pixflare uses a multi-layer caching strategy for maximum performance:
Layer 1: Cloudflare Edge Cache (automatic)
All images are cached at Cloudflare's 300+ edge locations globally. The Cache-Control: immutable header ensures aggressive caching. Cloudflare automatically purges these when you delete an image via the Cloudflare API.
Layer 2: R2 Storage (persistent)
Pre-generated variants are stored in R2. Once a variant is generated and stored, it's served from R2 forever (until the image is deleted). No transformation needed.
Layer 3: KV Cache (rate limiting)
KV is used to rate-limit on-demand variant generation per IP address. This prevents abuse (someone requesting 1000 different custom transformations).
Cache invalidation
When you delete an image, Pixflare purges all variants from Cloudflare's edge cache using the Cloudflare API. This ensures deleted images disappear immediately (no stale cache).
The implementation is in packages/api/src/lib/cache.ts.
You need these environment variables for cache purging:
CLOUDFLARE_ZONE_ID: Your Cloudflare zone IDCLOUDFLARE_API_TOKEN: API token with cache purge permissions
Requirements
R2 custom domain (required)
Image transformations only work if you configure a custom domain for your R2 bucket. Without it, Pixflare falls back to serving original images.
Setup:
- Go to your R2 bucket in the Cloudflare dashboard
- Click Settings
- Add a custom domain (e.g., r2.yourdomain.com)
- Configure DNS as instructed by Cloudflare
- Add the domain to your config:
[vars]
R2_CUSTOM_DOMAIN = "r2.yourdomain.com"This is required because Cloudflare Image Resizing needs a public URL to fetch the original image.
Cloudflare Workers paid plan (optional)
Background variant generation via queues requires a Workers paid plan (around $5/month). Without it, transformations still work perfectly fine via on-demand generation. You just won't get persistent storage of variants in R2.
Free tier: On-demand transformation works, variants not stored Paid tier: On-demand + background storage for faster future requests
To enable queues, add this to your wrangler.toml:
[[queues.producers]]
binding = "VARIANT_QUEUE"
queue = "pixflare-image-processing-queue"
[[queues.consumers]]
queue = "pixflare-image-processing-queue"
max_batch_size = 5
max_batch_timeout = 30
max_retries = 3And set the environment variable:
[vars]
ENABLE_QUEUES = "true"Environment variables summary
Required:
R2_CUSTOM_DOMAIN: Custom domain for your R2 bucketALLOWED_VARIANTS: JSON array of allowed preset namesDEFAULT_VARIANT: Preset to use when none specified
Optional:
ENABLE_QUEUES: Enable background variant generation (requires paid plan)CLOUDFLARE_ZONE_ID: For cache purging on deleteCLOUDFLARE_API_TOKEN: For cache purging on delete
See the full list in .env.example.
Advanced Configuration
Beyond the simple width-based presets, you can configure transformations with many options. These are defined in the transform object of your preset configuration.
Fit modes
The fit option controls how images are resized:
scale-down(default): Shrink to fit within dimensions, never enlarge, preserve aspect ratiocontain: Fit within dimensions, may add letterboxing, preserve aspect ratiocover: Fill dimensions exactly, crop if neededcrop: Same as cover but respects gravity settingpad: Fit within dimensions with background paddingsqueeze: Stretch or compress to fill (distorts the image)
Most presets use scale-down because it's safe and predictable. Use cover for thumbnails where you want exact dimensions (like the thumb and og-image presets).
Output formats
The format option forces a specific output format:
avif: Best compression, newer browsers onlywebp: Good compression, wide browser supportjpeg: Universal compatibilitybaseline-jpeg: Progressive JPEG (loads gradually)png: Lossless with transparency supportundefined(default): Auto-negotiate based on browser support (recommended)
In most cases, leave format undefined. Cloudflare will automatically serve AVIF to modern browsers and WebP to older ones.
Quality
The quality option controls compression (1-100):
- 100: Maximum quality (largest file)
- 90: High quality, minimal artifacts
- 85: Default, good balance
- 75: Medium quality, smaller files
- 60: Low quality, very small files
- 40: Minimum quality (heavy artifacts)
Higher quality means larger files. The default (85) is a good balance for most use cases.
Gravity and focus
When using cover or crop fit modes, gravity controls where the image is cropped:
auto(default): AI-detected important areas (faces, subjects, etc)face: Focus on facescenter: Center cropleft,right,top,bottom: Edge-aligned crops{x: 0.5, y: 0.5}: Custom coordinates (0.0-1.0 range)
The thumb and og-image presets use auto gravity for smart cropping.
Effects
You can apply visual effects:
blur: 1-250 (blur radius in pixels)sharpen: 0-10 (sharpening amount)brightness,contrast,saturation,gamma: Adjust image properties
These are rarely needed but available if you want to add filters.
Metadata handling
The metadata option controls EXIF data:
none(default): Strip all metadata (smaller files, better privacy)copyright: Keep copyright info onlykeep: Preserve all metadata
Pixflare strips metadata by default to reduce file size and protect user privacy (EXIF can contain GPS coordinates, camera info, etc).
Size limits
Cloudflare Image Resizing has these limits:
- Maximum width: 4096px
- Maximum height: 4096px
- Maximum DPR (device pixel ratio): 3
Requests exceeding these limits will fail.
Example custom preset
Here's a preset for high-quality product photos with specific dimensions:
'product-hero': {
description: 'Product page hero image',
label: 'Product Hero - 1400x900',
transform: {
width: 1400,
height: 900,
fit: 'cover',
gravity: 'auto',
quality: 90,
format: undefined, // Auto-negotiate
metadata: 'none',
},
}All available options are typed in packages/config/src/image-transforms.ts.
Security & Performance
Rate limiting
On-demand variant generation is rate-limited per client IP to prevent abuse. By default, the limit is 100 requests per 60 seconds.
If someone exceeds this, they get a 429 Too Many Requests error. Pre-generated variants (already in R2) are not rate-limited.
The rate limiting logic is in packages/api/src/lib/rate-limit.ts.
Privacy controls
Private images require authentication before serving:
- API keys (checked via database lookup)
- JWT tokens (from Cloudflare Access or Auth.js)
- Owner verification (must be authenticated as the image owner)
The authentication check happens before fetching the image from R2, preventing TOCTOU vulnerabilities.
Performance optimizations
Pixflare uses several strategies to maximize performance:
1. Immutable caching
Images use Cache-Control: public, max-age=31536000, immutable headers. This tells browsers and CDNs to cache aggressively (1 year). Since image URLs never change, this is safe.
2. Edge delivery
All images are served via Cloudflare's global CDN network. Users always fetch from the nearest edge location.
3. Format auto-negotiation
Cloudflare automatically serves AVIF (60% smaller than JPEG) to modern browsers and WebP (25% smaller) to older ones. You get this for free without configuration.
4. Lazy generation
Variants are only generated when requested, not on upload. This saves storage and processing time. Most images never need all variants.
5. Background persistence
Once a variant is requested, it's stored in R2 for instant future delivery. No transformation needed on subsequent requests.
6. CORS optimization
CORS headers allow images to be embedded anywhere:
'Access-Control-Allow-Origin': '*'
'Access-Control-Max-Age': '86400' // Cache preflight for 24 hoursThis prevents unnecessary preflight requests.
Monitoring
Pixflare tracks image serves using Cloudflare Analytics Engine. You can see:
- Which variants are most popular
- Total bandwidth saved by transformations
- Cache hit rates
- Geographic distribution of requests
This helps you optimize your preset configuration based on actual usage.