DEV Community

octave Nkurunziza
octave Nkurunziza

Posted on Originally published at blog.hubfly.space

How to resize and watermark product photos with just a URL

TL;DR

  • What you get: one upload per photo, and every thumbnail, WebP version and watermarked copy from a URL like …/image?w=400&h=400&fit=cover&format=webp.
  • Who it's for: shops, marketplaces and apps that show user or product photos, and don't want to run an image server.
  • Time: about 10 minutes in the dashboard.
  • Cost: transforms are free. You pay only for storing originals and cached variants, at the Standard storage rate of about $0.011 per GB-month.

Every product photo on your site needs four or five sizes: a thumbnail, a card, a full view, a social preview. Most teams either resize on upload with a script that breaks the day design changes a size, or run an image server they then have to patch and scale. Both cost you time, and slow pages cost you sales.

In this guide you'll store each photo once in Hubfly space Storage and ask for any size, format or watermark straight from the URL. Your designer can change the card size tomorrow without anyone re-processing a single file.

Try it on Hubfly space → (free tier, no card needed)

What the feature does

Hubfly space Storage is S3-compatible object storage. When you register an uploaded file as a managed asset, it gets delivery links and an image endpoint. Add query parameters to that endpoint and the service renders the variant on the first request, then caches it in your bucket.

Parameter What it does Limits
w, h Output width and height 1–4,096 px each, 16 megapixels max
fit inside (default), contain, cover, fill Anything except inside needs both w and h
format, quality webp (default), jpeg, png; quality 1–100 (default 80)
fx, fy Focal point for cover crops, 0 to 1 Defaults to the centre
rotate, flip 0/90/180/270; h, v or hv
blur, sharpen, brightness, contrast, saturation Effects Service-side bounds
wm_text or wm_key One text or image watermark, placed with wm_x, wm_y, wm_opacity Image watermarks must be saved as allowed first

Full reference: Storage assets and images.

Walkthrough: a product image pipeline in 10 minutes

1. Create a Storage project

In the dashboard, click Create Project, name it (for example shop-media), pick the Storage platform and choose a region. A Storage project keeps your buckets, keys and delivery settings apart from your app's containers, so you can give the app a key that only touches images.

2. Create a bucket

Click Create bucket and call it product-images. A bucket is the folder your app will write to; keeping product photos in their own bucket lets you scope a key to just that bucket later.

3. Upload a photo as a managed file

Click Upload files, drop in a photo and leave Managed file ticked. That tick is what registers the file as an asset with delivery links and an image endpoint. A plain S3 object doesn't get them automatically.

The Hubfly space Storage upload dialog with the Managed file box ticked for mug-blue.jpg

4. Choose who can see it

Open the file and go to Delivery. New assets start as Protected: every link needs the asset's access key or a temporary share link, and a request without one gets 401 access key required. For catalogue photos that anyone may see, switch Link access to Public. Keep customer uploads (IDs, invoices, private galleries) on Protected, and give them random object keys: a Protected viewer page still shows the file name and size to anyone with the link.

5. Build your image URLs

Your asset URL looks like https://<storage-node>/product-images/<asset-id>. That address opens a viewer page. For image bytes, add /image and your parameters:

https://<storage-node>/product-images/<asset-id>/image?w=400&h=400&fit=cover&format=webp
Enter fullscreen mode Exit fullscreen mode

The Transform & Cache tab has a form for every parameter and a Preview transformation button that builds the URL for you.

The Transform & Cache tab with fields for width, quality, focal point, effects, watermark position, fit and format

Use the URLs in a responsive image tag so each screen downloads only what it needs:

<img
  src="https://<storage-node>/product-images/<asset-id>/image?w=800&format=webp"
  srcset="https://<storage-node>/product-images/<asset-id>/image?w=400&format=webp 400w,
          https://<storage-node>/product-images/<asset-id>/image?w=800&format=webp 800w,
          https://<storage-node>/product-images/<asset-id>/image?w=1600&format=webp 1600w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Blue ceramic mug on a wooden table">
Enter fullscreen mode Exit fullscreen mode

For a social preview card, ask for the exact shape: ?w=1200&h=630&fit=cover&format=jpeg&quality=85.

6. Add a watermark

Add text and placement to the same URL. wm_x and wm_y go from 0 to 1, so 0.9/0.9 puts the mark near the bottom-right corner:

…/image?w=800&h=500&fit=cover&format=webp&wm_text=ACME%20Shop&wm_opacity=0.6
Enter fullscreen mode Exit fullscreen mode

An 800×500 WebP product photo of a blue mug with an ACME Shop text watermark in the bottom-right corner

For a logo instead of text, upload it in Transform & Cache, click Save allowed watermark, and use wm_key. Only a saved key works in public URLs, so a public link can't pull in any other file as a watermark.

7. Check the cache

The same tab lists every cached variant with its size and expiry, and lets you delete one. Identical URLs reuse the cached file instead of creating a new variant. On each image you can keep variants for the bucket default, a custom number of hours, or forever.

The cache retention setting and the list of 11 cached variants totalling 78 KB

Doing it from your app

The same flow works through the Storage API. Here are the routes from the API reference, each under https://api.hubfly.space/api/v1/projects/:projectId/storage/:

Step Route Key fields
Get a short-lived upload URL POST objects/upload-session bucket, objectKey, contentType → upload_url, expires_at
Upload the bytes PUT to upload_url before expires_at
Register the asset POST assets/create bucket, objectKey, title → asset ID and URL
Make it public PUT assets/update assetId, accessMode: "public"
Share a private file POST assets/share-token assetId, ttlSeconds

The upload session matters: your browser code never sees your S3 secret, only a URL that expires.

What can go wrong

  1. The image comes back at full size. You added parameters to the viewer page or the /media link. Transforms only work on the /image path.
  2. "unsupported or repeated image parameter". The service rejects unknown and duplicate parameters. Don't pass your own tracking or cache-busting query strings to image URLs.
  3. "fit requires both dimensions" or a size error. cover, contain and fill need w and h; each side must be 1–4,096 px and the output 16 MP or less.

A fourth one shows up later: each image allows 100 distinct transforms per UTC day. Pick a fixed set of sizes (say 400, 800, 1600) instead of the exact pixel width of every screen.

Limits and pricing, honestly (October 2026)

  • Transforms: free. Variants are stored under .hubfly-images/ and count toward your storage like any other file.
  • Storage rate: the pricing page lists standard storage at $0.00001536 per GB-hour. Over a 730-hour month that's 0.00001536 × 730 = $0.0112 per GB.
  • Example: 2,000 product photos at 1.5 MB is 3.0 GB. Four WebP sizes at an assumed 80 KB each (WebP product shots usually land between 20 and 100 KB) add 2,000 × 4 × 80 KB = 0.64 GB. Total 3.64 GB × $0.0112128 ≈ $0.04 a month. A full 100 GB project quota would be about $1.12 a month.
  • Bandwidth: delivery is metered, and you can follow it under Activity & usage in the dashboard.
  • Free tier: you can sign up and build this without a card; the free tier also includes 360 compute hours a month for your app.
  • Security: traffic uses HTTPS, but object bodies are not encrypted at rest. Encrypt sensitive files in your app first.

Who it's not for

  • You need AVIF or animated output. Formats are WebP, JPEG and PNG, and animated sources use the first frame.
  • You generate a different size per user or viewport. The 100-per-day limit will bite; resize on upload instead.
  • You need private photos in long-lived embeds. Protected links rely on an access key or short share links (15 minutes to 24 hours in the dashboard). A dedicated media CDN with signed URLs may suit you better.

Takeaway

Upload once, describe the size in the URL, and let the cache do the rest. Keep a short list of sizes, keep private files Protected, and watch storage rather than per-image fees.

Read next: Isolated SaaS tenants with sub-accounts · Deploy a docker-compose.yml as-is · Best Heroku alternatives in 2026

Go further on Hubfly space

  • Run the shop itself on Hubfly space Cell: deploy your app and database from a Compose file next to your images.
  • One bucket per customer: if you sell a SaaS, give each tenant its own space with sub-accounts.
  • Video too: the same Storage project turns uploads into HLS streams at 360p to 1080p (video docs).

Start free on Hubfly space, 360 hours included →

Top comments (0)