
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
| Approach | Survives a redeploy | Scales across instances | Cost of downloads | Verdict |
|---|---|---|---|---|
| App server's local disk | No on most platforms | No | Part of your app's traffic | Avoid |
| Database column | Yes | Yes | Every read goes through the database | Only for tiny files |
| Object storage with presigned URLs | Yes | Yes | Set by the store's egress price | The usual answer |
| Hosted upload service | Yes | Yes | Set by the service | Fine, 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
- The browser asks your server for permission to upload, sending the file name, size and type.
- Your server checks the session, the size and the allowed types, then generates a random object key. It stores a row with the status
pendingand returns a presigned PUT URL for that one key. - 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.
- 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. - 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
AbortIncompleteMultipartUploadto clean them up, and the same idea applies on other stores. - Orphans. Rows still
pendingafter 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
| Store | Price | Egress | Locations | Fits |
|---|---|---|---|---|
| Hetzner | €6.49 a month excl. VAT, including 1 TB stored and 1 TB of egress | 1 TB included, then billed | Falkenstein, Nuremberg, Helsinki | Most small and mid-size apps |
| Impossible Cloud | €7.99 per TB a month | No egress or API fees, under a fair use policy | European data centres | Apps with heavy downloads |
| OVHcloud | See its price list | No egress fees | Not stated | Teams already on OVHcloud |
| Exoscale | See its price list | Outbound traffic billed, inbound free | 8 zones in 5 European countries | Files that must stay in one country |
| Supabase Storage | Part of a Supabase plan | Part of a Supabase plan | European regions, company in Singapore | Apps 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
- Amazon S3: presigned URLs, multipart upload, CORS and deleting incomplete multipart uploads
- OWASP File Upload Cheat Sheet
- Heroku dynos and the ephemeral filesystem
- Hetzner Object Storage
- Impossible Cloud pricing
- OVHcloud Object Storage
- Exoscale Object Storage
- Supabase Storage S3 compatibility and resumable uploads
- Eurobase Storage, Eurobase containers, Eurobase Workers, Supabase on Eurobase and Eurobase plans
Keep reading
European alternatives to Vercel Blob for user uploads in 2026Where to keep avatars, documents and other files your users upload once you leave Vercel Blob: S3-compatible stores in Europe, what they cost and how the upload flow changes.Storage9 min read
Vercel or Netlify for a European team in 2026Regions, default function locations, seat pricing, credits and contracts compared for teams in Europe, plus what to pick when the provider itself has to be European.Comparisons8 min read
