Skip to content

Image Transformations

Fluxbase provides on-the-fly image transformation for files stored in Storage buckets. Transform images by adding query parameters to download URLs - resize, crop, convert formats, and adjust quality without modifying original files.

Image transformations are applied at download time:

  • Resize - Scale images to specific dimensions
  • Crop - Extract portions of images with different fit modes
  • Format conversion - Convert between JPEG, PNG, WebP, and AVIF
  • Quality adjustment - Control output file size vs quality

Original files remain unchanged. Transformed images can be cached for performance.

Add these parameters to any storage file URL:

ParameterAliasDescriptionExample
widthwTarget width in pixelsw=300
heighthTarget height in pixelsh=200
formatfmtOutput formatfmt=webp
qualityqQuality 1-100 (default: 80)q=85
fit-Fit mode (see below)fit=cover
# Resize to 300x200
/api/v1/storage/images/photo.jpg?w=300&h=200
# Resize width only (height scales proportionally)
/api/v1/storage/images/photo.jpg?w=300
# Convert to WebP
/api/v1/storage/images/photo.jpg?fmt=webp
# High quality WebP thumbnail
/api/v1/storage/images/photo.jpg?w=150&fmt=webp&q=90
# All options combined
/api/v1/storage/images/photo.jpg?w=300&h=200&fmt=webp&q=85&fit=cover

The fit parameter controls how images are resized to match target dimensions:

ModeDescriptionUse Case
coverFill target dimensions, crop excessThumbnails, profile pictures
containFit within dimensions, letterboxProduct images, galleries
fillStretch to fill exactly (may distort)Backgrounds
insideScale down only, never upEnsure images aren’t enlarged
outsideScale to be at least target sizeCover backgrounds

Original image: 800×600

# cover (default) - fills 200x200, crops excess
?w=200&h=200&fit=cover
Result: 200×200 (cropped from center)
# contain - fits within 200x200, maintains aspect ratio
?w=200&h=200&fit=contain
Result: 200×150 (with letterboxing if background added)
# fill - stretches to exactly 200x200
?w=200&h=200&fit=fill
Result: 200×200 (may appear stretched)
# inside - scales down to fit, won't scale up
?w=1000&h=1000&fit=inside
Result: 800×600 (unchanged, already fits)
# outside - scales to cover target, may exceed
?w=200&h=200&fit=outside
Result: 267×200 (scaled to cover, not cropped)
FormatMIME Type
JPEGimage/jpeg
PNGimage/png
WebPimage/webp
GIFimage/gif
TIFFimage/tiff
BMPimage/bmp
AVIFimage/avif
SVGimage/svg+xml
FormatParameterBest For
WebPfmt=webpModern browsers, best compression
JPEGfmt=jpgPhotos, compatibility
PNGfmt=pngTransparency, graphics
AVIFfmt=avifBest compression, modern browsers
import { FluxbaseClient } from '@fluxbase/sdk'
const client = new FluxbaseClient({ url: 'http://localhost:8080' })
const storage = client.storage
// Get transform URL (synchronous)
const url = storage.from('images').getTransformUrl('photo.jpg', {
width: 300,
height: 200,
format: 'webp',
quality: 85,
fit: 'cover'
})
// Result: http://localhost:8080/api/v1/storage/images/photo.jpg?w=300&h=200&fmt=webp&q=85&fit=cover
// Download transformed image
const { data, error } = await storage
.from('images')
.download('photo.jpg', {
transform: {
width: 300,
height: 200,
format: 'webp'
}
})
// Create signed URL with transforms
const { data: signedUrl } = await storage
.from('images')
.createSignedUrl('photo.jpg', {
expiresIn: 3600, // 1 hour
transform: {
width: 400,
format: 'webp'
}
})
import {
useStorageTransformUrl,
useStorageSignedUrl
} from '@fluxbase/sdk-react'
function ImageGallery() {
// Get transform URL (synchronous, no network request)
const thumbnailUrl = useStorageTransformUrl('images', 'photo.jpg', {
width: 150,
height: 150,
format: 'webp',
fit: 'cover'
})
return <img src={thumbnailUrl} alt="Thumbnail" />
}
function ProtectedImage() {
// Get signed URL with transforms
const { data: signedUrl, isLoading } = useStorageSignedUrl(
'private-images',
'photo.jpg',
{
expiresIn: 3600,
transform: {
width: 800,
format: 'webp',
quality: 90
}
}
)
if (isLoading) return <div>Loading...</div>
return <img src={signedUrl} alt="Protected" />
}

Generate multiple sizes for responsive images:

function ResponsiveImage({ path }: { path: string }) {
const storage = useStorage()
const srcSet = [
{ width: 320, descriptor: '320w' },
{ width: 640, descriptor: '640w' },
{ width: 1280, descriptor: '1280w' },
].map(({ width, descriptor }) => {
const url = storage.from('images').getTransformUrl(path, {
width,
format: 'webp'
})
return `${url} ${descriptor}`
}).join(', ')
return (
<img
srcSet={srcSet}
sizes="(max-width: 640px) 320px, (max-width: 1280px) 640px, 1280px"
src={storage.from('images').getTransformUrl(path, { width: 640, format: 'webp' })}
alt=""
/>
)
}

When available, transform parameters will be included in the signed URL signature, preventing tampering:

// PLANNED: Create signed URL with transforms
const { data } = await storage
.from('private-bucket')
.createSignedUrl('image.jpg', {
expiresIn: 3600,
transform: {
width: 400,
height: 300,
format: 'webp'
}
})
// Signed URL will include transform params in signature
// Modifying ?w=400 to ?w=800 will invalidate the signature

Configure image transformations in your Fluxbase config:

storage:
transforms:
enabled: true
default_quality: 80 # Default quality when not specified
max_width: 4096 # Maximum allowed width
max_height: 4096 # Maximum allowed height
allowed_formats: # Allowed output formats
- webp
- jpg
- png
- avif
cache_ttl: 86400 # Cache duration in seconds (24 hours)
VariableDescriptionDefault
FLUXBASE_STORAGE_TRANSFORMS_ENABLEDEnable transformstrue
FLUXBASE_STORAGE_TRANSFORMS_DEFAULT_QUALITYDefault quality80
FLUXBASE_STORAGE_TRANSFORMS_MAX_WIDTHMax width4096
FLUXBASE_STORAGE_TRANSFORMS_MAX_HEIGHTMax height4096
FLUXBASE_STORAGE_TRANSFORMS_MAX_TOTAL_PIXELSMax total pixels16000000
FLUXBASE_STORAGE_TRANSFORMS_BUCKET_SIZEDimension rounding50
FLUXBASE_STORAGE_TRANSFORMS_RATE_LIMITTransforms/min/user60
FLUXBASE_STORAGE_TRANSFORMS_TIMEOUTMax transform duration30s
FLUXBASE_STORAGE_TRANSFORMS_MAX_CONCURRENTMax concurrent transforms4
FLUXBASE_STORAGE_TRANSFORMS_CACHE_ENABLEDEnable cachingtrue
FLUXBASE_STORAGE_TRANSFORMS_CACHE_TTLCache TTL24h
FLUXBASE_STORAGE_TRANSFORMS_CACHE_MAX_SIZEMax cache size1073741824

Transformed images are cached in an internal _transform_cache bucket to avoid reprocessing:

  1. First request - Image is transformed and stored in cache
  2. Subsequent requests - Cached version is served instantly
  3. Cache eviction - LRU (Least Recently Used) entries are evicted when cache reaches max size
  4. TTL expiration - Cached transforms expire after the configured TTL (default: 24 hours)
sha256({bucket}/{path}:{width}:{height}:{format}:{quality}:{fit})

The cache key is a SHA256 hash of the transform parameters, ensuring unique cache entries for each variation.

To reduce cache fragmentation and improve hit rates, dimensions are rounded to the nearest bucket size (default: 50px):

  • Request: ?w=147&h=203 → Actual: ?w=150&h=200
  • Request: ?w=320&h=240 → Actual: ?w=300&h=250

This prevents attackers from busting the cache with many slightly different dimension requests.

  1. Use WebP - Best compression for modern browsers
  2. Specify dimensions - Prevents unnecessary large transforms
  3. Use signed URLs - For private buckets and CDN caching
  4. Set appropriate cache headers - Let browsers and CDNs cache
// Good: Specific dimensions and format
storage.from('images').getTransformUrl('photo.jpg', {
width: 300,
height: 200,
format: 'webp'
})
// Avoid: Only format conversion (transforms entire image)
storage.from('images').getTransformUrl('photo.jpg', {
format: 'webp' // Still transforms full-size image
})
Terminal window
# Basic resize
curl "http://localhost:8080/api/v1/storage/images/photo.jpg?w=300&h=200"
# Format conversion
curl "http://localhost:8080/api/v1/storage/images/photo.jpg?fmt=webp"
# All options
curl "http://localhost:8080/api/v1/storage/images/photo.jpg?w=300&h=200&fmt=webp&q=85&fit=cover"
Terminal window
curl "http://localhost:8080/api/v1/storage/private-bucket/photo.jpg?w=300&fmt=webp" \
-H "Authorization: Bearer YOUR_TOKEN"
Terminal window
# Create signed URL (via API)
curl -X POST "http://localhost:8080/api/v1/storage/images/sign/photo.jpg" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"expires_in": 3600,
"transform": {
"width": 400,
"format": "webp"
}
}'
# Response
{
"signed_url": "http://localhost:8080/api/v1/storage/images/photo.jpg?w=400&fmt=webp&token=xxx&expires=xxx"
}
  • Verify the file is an image (check Content-Type)
  • Ensure transforms are enabled in config
  • Check that requested dimensions are within limits
  • Increase quality parameter (default is 80)
  • Use appropriate format (JPEG for photos, PNG for graphics)
  • Avoid upscaling (use fit=inside to prevent)
  • First requests are slower (cache miss)
  • Large source images take longer
  • Consider pre-generating common sizes
  • AVIF requires libvips with AVIF support
  • Check server logs for codec errors
  • Fall back to WebP for broad compatibility
  • Reduce max dimensions in config
  • Process large images in batches
  • Increase server memory allocation
LimitDefaultConfigurable
Max width4096pxYes
Max height4096pxYes
Max total pixels16MYes
Max input sizeServer memoryNo
Rate limit60/min/userYes
Concurrent transforms4Yes
Supported formatsSee abovePartial

Fluxbase implements several security measures to protect against abuse:

  • Dimension limits - Prevents excessively large output images
  • Total pixel limit - Prevents memory exhaustion (default: 16 megapixels)
  • Rate limiting - Limits transforms per user per minute (default: 60)
  • Concurrency limit - Limits simultaneous transforms (default: 4)
  • Dimension bucketing - Rounds dimensions to nearest 50px to reduce cache-busting attacks
  • LRU eviction - Automatically removes oldest entries when cache is full
  • TTL expiration - Cached transforms expire after 24 hours by default
  • Directory traversal protection - All file paths are validated and sanitized
  • RLS enforcement - Storage permissions are checked before any transform