Serving images well is deceptively fiddly: a phone shouldn't download a 4000px hero, an <img> wants a srcset, and WebP/AVIF cut bytes dramatically — but only if something resizes and re-encodes the originals. The usual answer is a native library like libvips or sharp, or an external image CDN. onvibe apps can't bundle native code, so the platform does the resizing for you, two ways.
Resize when you serve — the CDN
Store an original once, then ask for any size in the URL. The variant is generated on first request and cached on images.onvibe.run; it's a regenerable cache, so it never counts against your storage quota.
const { key } = await uploadFile(await file.arrayBuffer(), file.type, file.name);
const thumb = await imageUrl(key, { w: 320, h: 320, fit: "cover" });
const srcset = await imageSrcset(key, [320, 640, 1024]); // one call, responsive
You pick w, h, fit, format (WebP by default, AVIF if you want) and quality per request — no need to decide sizes up front. The URLs are signed per project, so only your app can request variants of your own uploads.
Resize when you upload — stored variants
Sometimes you want a fixed set of sizes as real, permanent files. Generate them at upload time and get back public URLs:
const { variants } = await uploadImage(bytes, {
variants: { thumb: { w: 200, h: 200 }, card: { w: 800, format: "webp" } },
});
// variants.card.url → straight into <img src>
resizeUpload(key, { variants, discardOriginal: true }) does the same for something you already stored — handy to shrink a huge original before it eats your quota.
Which to reach for
On-the-fly wins when sizes are open-ended or you want a srcset — it's cache, not storage. Preprocessing wins when the sizes are known and always needed, or when you want to throw away a large original. Both share the same knobs (w/h/fit/format/quality) and the same engine; only where the bytes land differs.
Full reference: Resizing & image CDN.