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.
The good news is the same as with accounts and payments: you don't need to understand storage infrastructure to do this safely. You need to avoid one common mistake — writing uploaded files to your own server or database — and confirm two things AI tools often skip by default: that access to a file is enforced by the storage provider, not by hiding its URL, and that the app checks what was actually uploaded rather than what the browser claimed to send.
If an AI tool generates code that saves an uploaded file's raw bytes directly into your application database, or writes it to a folder on the same server that runs your app, stop. That approach doesn't scale, is awkward to back up, and usually skips validation entirely. The fix below sends the file straight to a dedicated storage service instead, the same way hosted checkout keeps card numbers out of your own database.
The Safe Default: A Hosted Storage Service
Just as a hosted auth provider takes password storage off your plate, a hosted storage service takes file handling off your plate: it stores the bytes, serves them through a CDN, enforces size limits, and can apply access rules per file. Your app keeps a reference — a URL or file key — in its own database, not the file itself.
- Supabase Storage — a natural fit if you're already using Supabase for your database or Supabase Auth; the free tier includes 1 GB of file storage and 5 GB of egress, and you can attach the same Row Level Security model you already use for tables to storage buckets.
- Cloudinary — built specifically for images and video, with automatic resizing, format conversion, and optimization on delivery; the free plan gives 25 credits a month, where each credit covers 1,000 transformations, 1 GB of storage, or 1 GB of delivered bandwidth (shared across whichever you use most).
- UploadThing — a developer-friendly upload flow built around a simple client component and server-side callbacks; the free plan includes 2 GB of storage shared across the apps on your account, with unlimited uploads and downloads.
All three keep the file itself out of your application code path after the initial upload. Writing your own upload handler that streams files to your own disk or object storage bucket is possible and sometimes right for larger apps, but it's much more surface area to get wrong — start with a hosted service unless you have a specific reason not to.
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]. Do not write uploaded files to my own server disk or store their raw bytes in my database — only store the resulting URL or file key. Include: (1) an upload component that sends the file to the provider directly or through a signed upload URL, not through my backend as a raw buffer, (2) a maximum file size and an allowed file type list enforced by the provider or my server, not only by the file picker's `accept` attribute, (3) a default of private access for this file, with a signed, time-limited URL used to display it, unless there's a specific reason it should be public, and (4) a rule that only the file's owner can replace or delete it. Tell me which of these settings are configured in the provider's dashboard versus in my code.
When the AI returns code, check for these things before you consider it done:
- The file never passes through your own database as a blob — your database stores a URL or file key, the provider stores 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.