Storage · · 8 min read

How to store user uploads in a web app in 2026

Why avatars, PDFs and other user files belong in object storage, how presigned uploads work, and which European stores fit an app that handles uploads.

Put the files in an S3-compatible object store, keep only a small record of each file in your database, and let the browser upload straight to the bucket through a short-lived presigned URL that your server signs after checking the user. Don't write uploads to the app server's own disk, and don't put the bytes in your database. For a European store, Hetzner Object Storage is a sensible default for moderate traffic, Impossible Cloud and OVHcloud suit apps whose users download a lot, and Exoscale fits files that must stay in a named country.

Where uploads can live at a glance

ApproachSurvives a redeployScales across instancesCost of downloadsVerdict
App server's local diskNo on most platformsNoPart of your app's trafficAvoid
Database columnYesYesEvery read goes through the databaseOnly for tiny files
Object storage with presigned URLsYesYesSet by the store's egress priceThe usual answer
Hosted upload serviceYesYesSet by the serviceFine, check who runs it

Why the app server's disk is the wrong place

Most hosting platforms treat the filesystem as scratch space. Heroku's documentation says each dyno gets its own ephemeral filesystem, and files one dyno writes aren't visible to the others. Container apps on Eurobase scale to zero when they get no traffic, and the next request starts a new instance, so don't count on files written locally.

A database column survives restarts, but it makes backups heavy and pushes every image view through your connection pool. Keep only who uploaded which file, its key, size, type and status there.

What to decide before you write the upload code

  • Who holds the contract for the bucket. Your customers' files sit with whoever signs the storage contract, so a European data centre run by a US company may not satisfy a customer who asks for a European provider. EU region vs EU company explains the difference.
  • What downloads cost. Avatars and shared documents are read far more often than they're written, so egress and per-request fees decide the bill more than storage does.
  • Public or private. Profile pictures can be public. Invoices, ID scans and anything personal should be private and served through signed links.
  • The largest file you accept. Small files can pass through your app; large ones should go straight to the bucket, and anything around 100 MB or more should use multipart upload.
  • How long files are kept. Decide when deleted accounts lose their files, and write it into your privacy policy. The GDPR hosting checklist covers the rest.

The upload flow that works

  1. The browser asks your server for permission to upload, sending the file name, size and type.
  2. Your server checks the session, the size and the allowed types, then generates a random object key. It stores a row with the status pending and returns a presigned PUT URL for that one key.
  3. The browser sends the file straight to the bucket with that URL. The file never passes through your app, so your request size and CPU limits don't apply to it.
  4. The browser tells your server the upload finished. The server reads the object's metadata with a HEAD request, checks the real size and type, and marks the row ready, or deletes the object if it doesn't match.
  5. Downloads go through your app: it checks access, then redirects to a presigned GET URL that expires in minutes.

A presigned URL names the bucket, the object key, the HTTP method and an expiry, and AWS notes it can be reused until it expires, so keep the expiry short. A checksum header on the upload lets the store verify the bytes that arrive.

The bucket needs a CORS rule that allows PUT from your own domain, or the browser blocks the upload. Confirm your store supports bucket CORS rules before you pick it, and keep the rule in your setup scripts so it survives a new bucket.

Security rules for uploaded files

The OWASP File Upload Cheat Sheet, applied to object storage:

  • Allow a short list of types. Check the extension against an allowlist, then check the file's signature, because the Content-Type header is whatever the browser sends.
  • Never use the user's file name as the key. OWASP recommends a random name such as a UUID. Keep the original name in your database if you need to show it.
  • Set a file size limit. Refuse larger requests before signing, and check the real size again after the upload.
  • Let only signed-in users with the right role upload.
  • Keep user files away from your app's own domain. Serving them from a separate host stops an uploaded HTML or SVG file from running scripts as your site.
  • Scan files you open or process. OWASP suggests an antivirus or sandbox, and re-encoding images with an up-to-date library.

Costs that catch people out

  • Abandoned multipart uploads. Parts of an upload that never completed still take up storage. AWS recommends a lifecycle rule with AbortIncompleteMultipartUpload to clean them up, and the same idea applies on other stores.
  • Orphans. Rows still pending after a day point at objects nobody uses. A daily cron job can delete them.
  • Hot public images. Put a CDN in front of the bucket rather than paying egress on every view.

European stores for user uploads

StorePriceEgressLocationsFits
Hetzner€6.49 a month excl. VAT, including 1 TB stored and 1 TB of egress1 TB included, then billedFalkenstein, Nuremberg, HelsinkiMost small and mid-size apps
Impossible Cloud€7.99 per TB a monthNo egress or API fees, under a fair use policyEuropean data centresApps with heavy downloads
OVHcloudSee its price listNo egress feesNot statedTeams already on OVHcloud
ExoscaleSee its price listOutbound traffic billed, inbound free8 zones in 5 European countriesFiles that must stay in one country
Supabase StoragePart of a Supabase planPart of a Supabase planEuropean regions, company in SingaporeApps that already use Supabase Auth

Prices are from each provider's own page, checked on 10 November 2026.

Once you turn on its S3 connection, Supabase Storage accepts S3 requests and presigned URLs, and its docs recommend resumable uploads over the TUS protocol for files larger than 6 MB or on unstable networks. By default your contract is with a Singapore company, Supabase Pte. Ltd., so weigh that against what your customers expect. For a fuller comparison of the S3 stores, read S3-compatible object storage in Europe.

Where Eurobase fits

Eurobase hosts the app that signs and checks uploads: frameworks such as Next.js run on V8 isolates and servers in Node.js, Python, Go and other languages run as containers, on European infrastructure run by Scaleway, a French provider. The business is registered in Warsaw, Poland. Your upload route can sign URLs for any S3 store above, and Supabase Storage is reachable from container apps and from apps on V8 isolates alike. A request body sent to your app can be up to 100 MiB, so larger files should use the presigned flow above.

Eurobase Storage is for files you manage yourself from the dashboard. Your app has no keys to write to those buckets, so your users' uploads need one of the stores above.

  • Best for: running the upload route and the rest of the app in Europe at a flat price, next to an S3 store you choose.
  • Price: a Free plan, then Starter at €10 and Pro at €20 a month + VAT. No usage is billed today: actions over a limit are refused, apps on V8 isolates and Functions stop until the monthly reset, and containers and static sites keep serving.
  • What it lacks: a bucket your app can write user uploads to, managed Postgres, and preview deployments. ISO 27001 and SOC 2 are still being worked on, not held.

Frequently asked questions

Should I store images in my database?

Usually no. Uploads bloat backups and send every view through your database. Store the file in object storage and keep its key, size, type and owner in a table.

Is it safe to let the browser upload straight to S3?

Yes, if your server signs each URL for one random key, after checking the user, with a short expiry. The browser never sees your access keys. Check the object's real size and type once it lands, and delete it if it doesn't match.

How do I handle very large files?

Use multipart upload, which splits a file into parts that upload separately and can be retried one at a time. AWS suggests it for objects of 100 MB or more and allows up to 10,000 parts per upload.

Do I need a European provider for GDPR?

GDPR doesn't require one. You do need a data processing agreement with the store, and a valid transfer mechanism if files leave the EU. If customers ask for a European company, choosing one now saves a migration later.

Sources

Deploy your app in Europe

Sign up