Skip to content

Image Transformations

AnvilBase bundles imgproxy for on-the-fly image processing — resize, crop, change format, adjust quality — so you store one original and serve any variant without keeping multiple copies.

How it works

You request a transform through the control plane at /storage/v1/render/image/{authenticated|public}/<bucket>/<path>. The control plane signs an imgproxy URL (HMAC over the processing options + the object’s S3 location), fetches the transformed image from the internal imgproxy sidecar server-side, and streams it back through the control-plane origin. The response is served from the control-plane origin you already talk to — it is never a redirect to imgproxy, so it works from any browser <img src>.

  • imgproxy is internal-only — it is never exposed on the host. Only the control plane knows IMGPROXY_KEY/IMGPROXY_SALT, so only it can mint valid transform URLs. This prevents resize-bombing and arbitrary source fetches.
  • The public variant serves transforms for objects in a public=true bucket, unauthenticated. The authenticated variant requires a valid key or JWT (and honors owner-prefix RLS on the bucket).

Configuring imgproxy URL signing

This signs the imgproxy transform URLs the control plane mints — it is unrelated to storage object signed URLs (createSignedUrl), which use a separate per-project HMAC key.

Set both IMGPROXY_KEY and IMGPROXY_SALT in .env (they must be set together):

Terminal window
IMGPROXY_KEY=$(openssl rand -hex 32)
IMGPROXY_SALT=$(openssl rand -hex 32)

Restart the stack after changing them. If they are unset, imgproxy runs in insecure (unsigned) mode — fine for local dev, but you should set both in production so the transform surface can’t be forged.

The endpoints

VariantPathAuth
PublicGET /v1/storage/<id>/render/image/public/<bucket>/<path>none (bucket must be public)
AuthenticatedGET /v1/storage/<id>/render/image/authenticated/<bucket>/<path>key/JWT

Query parameters

ParamMeaningExample
widthtarget width (px)width=400
heighttarget height (px)height=400
resizecover (default, crop to box) · contain (fit) · fill (force)resize=contain
qualityoutput quality, 1–100quality=80
formatorigin · webp · avif · jpeg · pngformat=webp

These map to imgproxy processing options (rs:fill:W:H, rs:fit:W:H, rs:force:W:H, q:N, and a format extension) under the hood.

From the SDK

Use getPublicUrl(path, { transform }) for a public bucket — it returns the render/image/public URL with no network call:

const { data } = db.storage.from('avatars').getPublicUrl('user-123/photo.jpg', {
transform: { width: 400, height: 400, resize: 'cover', format: 'webp' },
})
// data.publicUrl →
// https://<host>/v1/storage/<project>/render/image/public/avatars/user-123/photo.jpg
// ?width=400&height=400&resize=cover&format=webp

The same shape is available in every SDK (get_public_url / getPublicUrl), and a transformed URL is just <host>/v1/storage/<id>/render/image/... — drop it straight into an <img src> (for a public bucket) or fetch it server-side.

Why use it

  • Store once, serve many — keep the original; derive every size/format on demand.
  • Modern formats — emit WebP/AVIF to clients that support them, cutting bytes significantly.
  • Responsive images — generate srcset variants from one source.
  • Safety — signing prevents resize-bombing and arbitrary source fetches; the imgproxy sidecar stays off the public network.

Caching

Put a CDN or Traefik cache in front of the control plane so each variant is computed once. The transform URL is deterministic for a given source + options, which makes it cache-friendly.

An object’s uploaded cacheControl/Cache-Control directive is stored on the object and re-emitted on download, so a CDN/Traefik cache in front of the control plane honors it directly (see Working with objects → cacheControl).

Next: Edge Functions → Overview.