Why Uploads Deserve Extra Scrutiny
A file upload is one of the easiest features to ask AI for, and one of the easiest to get wrong in ways that don't show up until later. AI can produce a working file picker, an "Upload successful" message, and a photo that displays back on the page — and it will look finished. What's easy to miss: whether the file went somewhere that scales, whether "private" actually means private, and whether the app checked what the file really is instead of trusting what its name or extension claims.
A hosted service can reduce infrastructure work, but it does not make an upload safe by itself. Confirm that authorization is enforced on every read, replace, and delete; validate the received content rather than trusting the browser; set size and retention limits; and decide whether malware scanning or manual review is required for the file type and audience.
For most web apps, keep user file bytes in managed object storage and store only a file key plus metadata in the application database. Database blobs and server-proxied uploads can be valid in specialized designs, but they require explicit size, streaming, scanning, backup, and capacity decisions. Do not let AI choose either path accidentally.
The Safe Default: A Hosted Storage Service
A hosted storage service stores and serves the bytes and can enforce quotas and access rules. You must configure those controls: defaults differ, and a public bucket or URL remains public regardless of how random its name looks. Your app normally keeps a file key and trusted metadata in its database.
- Supabase Storage — a natural fit if you're already using Supabase for your database or Supabase Auth; its Aug 28 2026 free-plan table lists 1 GB of storage plus separate cached and uncached egress allowances. Storage access uses policies on
storage.objects; ownership alone does not grant or deny access. - Cloudinary — built for images and video, with transformations and optimized delivery. Its Aug 28 2026 free plan uses 25 shared monthly credits across transformations, storage, and bandwidth, so 25 credits is not 25 of each resource.
- UploadThing — a component-and-callback workflow whose Aug 28 2026 free plan lists 2 GB of storage. Its documentation says files are publicly readable by URL by default, while private-file features depend on configuration and plan; verify the access model before choosing it for private documents.
Direct-to-provider uploads can keep large buffers out of your application server, but callbacks, metadata, authorization, scanning, and deletion still form part of your code path. Choose a provider only after checking the required access model and current plan.
The Prompt to Ask For
Name the hosted service explicitly so AI doesn't default to writing a custom upload handler, and ask for access control and validation in the same prompt so neither is left as an afterthought:
Add file upload to my app using [Supabase Storage / Cloudinary / UploadThing] for [profile photos / documents / attachments]. Store the object in managed storage and keep only its provider key plus trusted metadata in my database. Include: (1) direct or signed upload where appropriate, (2) maximum size, count, and allowed types enforced by the provider or server, with server-side content inspection rather than only filename, MIME header, or the file picker's accept attribute, (3) private-by-default access with an expiring signed URL or authenticated delivery when the chosen provider and plan support it, (4) owner checks for read, replace, and delete, (5) a malware-scanning or quarantine decision appropriate to the file type, and (6) retention, cleanup, backup, and cost controls. Identify every provider setting, plan dependency, and assumption you cannot verify.
When the AI returns code, check for these things before you consider it done:
- The chosen storage boundary is explicit — normally the database stores a provider key and metadata while object storage holds the bytes.
- A size limit and type restriction are enforced by the provider or your server, not just suggested by the file picker — a browser's
acceptattribute is a hint to the operating system's file dialog, not a check on what actually gets sent. - Private files use signed or access-controlled URLs, not a public bucket with an unguessable name — the next section explains why that distinction matters.
- Only the uploading user can replace or delete their own file — the same ownership check you'd expect on any other user data.
Why a Hidden File URL Isn't the Same as a Private File
It's tempting to treat a long, random-looking file URL as private — nobody would guess it, so it feels safe. Don't rely on this. If the file lives in a public bucket, anyone who obtains that URL — from a shared link, a browser history, a referrer header, or a page that embeds the image — can view or download it forever, with no way for you to revoke access. An unguessable name changes who's likely to find the file; it does nothing to stop anyone who does.
A public marketing image and a user's private tax document are not the same kind of file, even if your app stores both in the same provider. Public assets — a logo, a shared blog header — can live in a public bucket with a plain URL. Anything tied to one user's account should default to private: access enforced by the storage provider's own rules (Supabase Storage policies, Cloudinary's signed delivery, UploadThing's access controls) and served through a URL that expires or that only resolves for the authenticated owner. Decide this per file type up front, not after a user finds someone else's file.
Test this the same way you'd test account isolation: upload a file as Account A, then try to fetch it directly — by URL, and by calling whatever endpoint serves it — while signed in as Account B, and again while signed out entirely. If the file was meant to be private, both attempts should fail. A file that only "looks" private because its URL is long isn't private — it's unlisted.
Show me exactly how access is controlled for each file type my app stores. For anything tied to a single user's account, confirm the storage provider itself denies access to other users and to signed-out visitors — not just that the app doesn't link to the file. Give me a test: upload a file as one account, then show the requests I'd run to try to access it as a different account and while signed out, and what response each should return.
Validate What You Received, Not What Was Claimed
A file's name and extension are whatever the person uploading it typed, and the browser's reported content type is whatever the operating system guessed — neither is a guarantee of what the file actually is. A file named photo.jpg can contain anything. Ask AI to check the file's real content, not its label, before your app treats it as an image, a PDF, or anything else it displays or processes.
- Check content, not just the extension. Hosted image services like Cloudinary re-encode uploaded images during their transformation step, which naturally rejects most disguised or malformed files; if you're storing arbitrary file types, ask AI to verify the file's actual signature rather than trusting its name.
- Strip metadata from photos before they're shown publicly. Phone photos commonly embed GPS coordinates and device details in EXIF metadata. If a photo will be visible to anyone besides its owner, confirm that metadata is stripped — most image transformation services do this automatically, but don't assume it without checking.
- Re-check size after upload, not only before. A file size limit shown in your UI is a courtesy to the user; enforce the real limit on the provider or server side, since a request can be sent without ever going through your form.
Keeping Storage and Cost Under Control
Unlike most features, uploads add to a bill that grows with usage and never shrinks on its own — every file kept is file storage you keep paying for, on every provider's free tier and beyond. Two habits keep this from surprising you:
- Delete the file when its owning record is deleted. A common gap: a user deletes their profile, a post, or a listing, and the record disappears from your database while its uploaded file quietly stays in storage forever. Ask AI to wire up file deletion alongside record deletion, not as a separate step you might forget.
- Watch usage against the free tier before it becomes a problem. Supabase Storage's free tier is 1 GB, Cloudinary's is 25 credits a month, and UploadThing's is 2 GB shared across your apps — small enough that an active app can reach them within weeks. Check your provider's usage dashboard periodically rather than finding out when uploads silently start failing or a bill arrives.
Pre-Launch Uploads Checklist
- Hosted storage service, not your own server disk or database blob — your app stores a URL or file key, the provider stores the bytes.
- Size and type limits enforced by the provider or server — not only suggested by the file picker.
- Private files access-controlled at the provider, not merely served from an unguessable URL — verified with a two-account and signed-out test.
- Only the owner can replace or delete their own file — the same ownership rule as any other user data.
- File content checked, not just its claimed name or type — and metadata stripped from any photo shown publicly.
- File deletion wired to record deletion — so removing a post or account doesn't leave its files behind forever.
- Free-tier usage checked periodically — storage only grows, and every provider here has a modest free ceiling.
Related Guides
Adding User Accounts to Your Vibe Coded App
Most uploads belong to a signed-in user — hosted auth, per-user data access, and the two-account isolation test this guide builds on.
Adding Payments to Your Vibe Coded App
The other feature where an AI shortcut has real consequences — hosted checkout, webhooks, and what to verify before real money moves.
Growing Your Vibe Coded App
Adding features, handling real users, and keeping an eye on hosting and storage costs as your app grows.
Sanitizing Code and Data Before Sending to AI
What to scrub before pasting code or credentials into an AI conversation — including storage keys.