Store User Uploads in Your App
How to Handle User File Uploads in Your App
Handle user file uploads with a simple API. Secure storage, CDN delivery, and access controls built in.
Storing user uploads safely comes down to four responsibilities: validation, durable storage, fast delivery, and access control. Here's the architecture that handles all four without you operating storage infrastructure.
Step 1 — validate on your backend, never trust the client. Check the declared MIME type against the actual bytes (magic-number sniffing), enforce a size ceiling, and reject anything outside your allowlist. A user can rename evil.exe to cute.png; only server-side content inspection catches that.
Step 2 — mint a presigned upload URL. Your backend calls POST /v1/files/{folderId} with the file's metadata and gets back a presigned PUT URL and a file id. This is where your API key is used — as the raw Authorization header, on the server only.
Step 3 — upload the bytes. Either your backend streams them, or (better for large files) you return the presigned URL to the browser and let the client PUT directly, so a 50 MB video never tunnels through your API process. Then your backend calls POST /v1/files/confirm-upload with {ids:[id]} to finalize.
Step 4 — persist and serve. Save the returned file id (and CDN URL) in your database alongside the owning user id. For public assets like avatars, embed the permanent CDN URL directly and let the edge serve it. For anything private — invoices, ID documents, paid downloads — upload with the private flag and, when a user requests access, have your backend mint a signed URL (10-minute expiry) scoped to that one file.
Never hand the API key to the browser, and never let the client tell your backend where to store a file or which file to sign without an ownership check — that's how IDOR bugs leak other users' data.
A minimal Express handler tying it together:
app.post('/upload', auth, upload.single('file'), async (req, res) => {
validate(req.file); // step 1
const meta = await fl.post(`/v1/files/${FOLDER}`,
{ filesMetadata: [{ name: req.file.originalname }] }); // step 2
const { url, id } = meta.urls[0];
await fetch(url, { method: 'PUT', body: req.file.buffer }); // step 3
await fl.post('/v1/files/confirm-upload', { ids: [id] });
await db.saveUpload(req.user.id, id); // step 4
res.json({ id });
});This keeps your server out of the storage business while you retain full control over who can upload what, and who can read it back.
Benefits With No Complexity
Global CDN delivery
Edge-cached worldwide
Signed-URL security
What You Get
Unlimited files
Unlimited storage
Public + Private storage
CDN ready links
Prepaid credits
More coming soon
How files.link Works
1. Upload
2. Copy
3. Use Anywhere
Why Developers Choose files.link
A Better Way to Store & Deliver Files
| Self-managed (S3 + custom code) | Firebase Storage | Supabase Storage | files.link | |
|---|---|---|---|---|
| Built-in CDN delivery | ||||
| Presigned PUT keeps bytes off your server | ||||
| No storage infrastructure to operate | ||||
| Private files with signed URLs | ||||
| Permanent public URLs | ||||
| Single REST API, no SDK required |
Calculate Your Needs
Storage
Egress
CDN Bandwidth
Start Handling User Uploads
- Global CDN delivery
- Edge-cached worldwide
- Signed-URL security
- Unlimited files
- Unlimited storage
- Public + Private storage
- No subscription — prepaid credits keep spend predictable