How to Add File Uploads to an AI-Built App

AI coding tools will generate a working file upload form in seconds, but they skip the three things that actually matter: type and size validation, per-user storage scoping, and malware scanning. Here is how to fix all three, with a print shop's logo and signed-PDF uploads as the running example.

Steve Jefferson
Steve Jefferson
Developer Advocate
3 August 20261 min read

To add file uploads to an AI-built app, you need three things your AI tool almost never gives you by default: server-side validation of file type and size, storage rules that scope every file to the user who owns it, and a scan step before anything untrusted touches your app. Ask an AI coding assistant for "a file upload feature" and it will hand you a working demo. It will not hand you these three things. This guide walks through why, using a small business scenario, and shows the fix for each with real platform documentation.

The worked example: a print shop, a logo, and a signed proof

Consider a print shop that lets customers upload their own logo for custom merchandise, and later upload a signed proof PDF approving the final design before it goes to production. That is two upload flows: an image from an anonymous or logged-in customer, and a signed document tied to a specific order. Both are common enough that most "build me an app" prompts eventually need this pattern, whether it's a logo, a contract, an ID photo, or a spreadsheet of customer data.

Why the AI-generated version breaks

Ask ChatGPT, Claude, or an AI app builder to "let users upload a file to Supabase" or "add file upload to S3" and you typically get code that works the first time you test it, because you're the one uploading a small, well-behaved PNG from your own laptop. It falls apart once real customers with real files show up. Three problems show up over and over.

Mistake 1: No file type or size validation

The generated code usually checks nothing beyond "did a file arrive." The <input type="file" accept="image/*"> attribute in your HTML is a suggestion to the browser's file picker, not a security control. Anyone can rename payload.exe to logo.png, or upload a 900MB video where you expected a 200KB image, and the client-side accept attribute will not stop them.

The fix is validation on the server, not just the browser, and ideally validation your storage layer enforces on its own regardless of what your API code does. Supabase Storage lets you set both a size limit and an allowed MIME type list at the bucket level:

js
const { data, error } = await supabase.storage.createBucket('logos', {
  public: false,
  fileSizeLimit: 5242880, // 5MB, in bytes
  allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
})

Uploads that violate either limit are rejected by Supabase before your application code even runs, which matters because it protects you even if a future code change forgets to re-check. For the signed PDF flow, you'd create a separate bucket with allowedMimeTypes: ['application/pdf'] and a lower size ceiling, since a signed proof document has no business being 200MB.

If you're building on raw S3 instead of a backend-as-a-service, you don't get a bucket-level MIME allowlist the same way, so validation has to happen in the code that issues the upload URL and, ideally, again with a Lambda trigger that checks the actual file signature (its "magic bytes") after upload, not just the extension the client claimed.

Mistake 2: Unscoped writes to a shared bucket

This is the one that causes the most damage. AI-generated code frequently uploads every file to the same flat bucket path, something like uploads/logo-123.png, with no rule tying that file to the customer who uploaded it. The demo works because there's one test user. In production, customer A can often guess or enumerate customer B's file URL, or worse, an insecure API route lets any authenticated user read or overwrite any file regardless of who it belongs to.

Supabase Storage integrates with Postgres row level security on the storage.objects table, and by default it blocks all uploads until you write a policy explicitly allowing them. A policy that scopes uploads to a folder named after the user's own ID looks like this:

sql
create policy "Users can upload to their own folder"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'logos' and
  (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

Pair that with a matching select policy so users can only read files under their own folder, and the storage layer itself becomes the enforcement point, not a check you have to remember to add in every API route. This is the same principle covered in more depth in our guide on adding user accounts to an AI-built app: once you have real user identity, storage access should key off it everywhere, not just in your database queries.

On raw S3, the equivalent is a presigned URL generated per user, per object key, that only grants permission for that specific upload. According to AWS's documentation on presigned URLs, a presigned URL inherits the permissions of whoever generated it and is valid for a set expiration window, up to 7 days when using the CLI or SDKs. The critical detail AI-generated code frequently misses: the object key you sign should include the user ID or order ID, not a value the client controls, so one customer's presigned URL cannot be reused to overwrite another customer's file.

js
const command = new PutObjectCommand({
  Bucket: 'print-shop-uploads',
  Key: `${orderId}/${userId}/logo.png`,
  ContentType: 'image/png',
})
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 900 })

Note the ContentType is included in the signed parameters. AWS's guidance is clear that this locks the content type so the client can't swap in a different file type after the URL is issued, and a short expiresIn (900 seconds here, 15 minutes) limits how long a leaked URL stays useful.

Mistake 3: No malware or content scanning

The third gap is the one AI tools almost never mention unprompted: nobody scans the file after it lands. A validated MIME type and a properly scoped bucket path stop a lot of abuse, but they do not stop a legitimate-looking PDF from carrying an embedded exploit, or an image file with a polyglot payload designed to execute when your backend later processes it (resizing, thumbnailing, OCR, whatever you do with uploaded files server-side).

Supabase's own storage documentation does not include built-in malware scanning as of this writing, so if you're on that stack, the scan has to be a step you add yourself, typically triggered by a storage webhook or a background job that runs before a file is marked "usable" anywhere else in your app.

On AWS, there are two realistic paths. The manual route is a ClamAV-based Lambda that fires on S3's object-created event, scans the file into a temporary location, and moves it to a clean or quarantine prefix depending on the result, a pattern AWS's own developer blog has published a reference implementation for. The managed route is Amazon GuardDuty Malware Protection for S3, which scans newly uploaded objects in a chosen bucket automatically and can tag or quarantine infected files without you running any scanning infrastructure yourself.

Either way, the practical rule for the print shop example: a customer's logo or signed PDF should sit in a "pending" state, invisible to your staff-facing dashboard and not yet queued for printing, until the scan step clears it. That's a small state machine (uploaded, scanning, clean or flagged), not a one-line check, and it's exactly the kind of workflow step an AI code generator will skip unless you ask for it by name.

Picking Supabase Storage vs. S3 presigned URLs

Both are legitimate choices and the decision usually comes down to what else your app already uses.

Supabase Storage makes sense if you're already on Supabase for auth and your database, since row level security policies can reference the same auth.uid() your other tables use, and bucket-level MIME and size limits are configured in one place. It also handles resumable uploads for large files out of the box.

S3 presigned URLs make sense if you're already deep in AWS, need very large files, need fine-grained IAM control, or want to layer GuardDuty Malware Protection on top without adding a separate vendor. The tradeoff is you write more of the validation and scoping logic yourself, since S3 doesn't have a bucket-level MIME allowlist the way Supabase does.

If your app doesn't have a backend at all yet, that's a separate decision worth making first. See do you need a backend for your app for how file storage requirements factor into that call, and how to build an app with AI for the broader picture of what an AI-assisted build needs beyond the initial prompt.

A file upload security checklist before you ship

Before any upload feature in an AI-built app goes live, confirm:

  • File type is validated against an allowlist, enforced server-side or at the storage bucket level, not just via the HTML accept attribute.

  • File size has a hard ceiling appropriate to the use case (a logo does not need to be 500MB).

  • Every uploaded file's storage path or bucket policy is scoped to the specific user or order it belongs to, verified with row level security or per-object IAM permissions, not just "the API route checks a session."

  • Uploaded files sit in a pending or unscanned state until a malware or content check clears them, and that check actually runs somewhere, not just in a comment saying "TODO: add scanning."

  • Presigned URLs (if you're using them) have a short expiration and a locked content type, and the object key includes something the client cannot forge, like a server-issued order ID.

If you're using an AI app builder like Swarmz to scaffold the app, these same three fixes apply on top of whatever storage integration it generates. The builder gets you a working upload form fast; the validation, scoping, and scanning steps are still yours to add and verify, the same as they would be with hand-written code. Treat generated storage code the same way you'd treat any other AI output before shipping it: read it line by line, as covered in how to review AI-generated code before you ship it, and confirm it against your platform's actual documentation rather than trusting that it matches what the model was trained on.

Once uploads work, the next common addition to this kind of app is charging for the service those files support, covered in how to add payments to an AI-built app, and eventually getting the whole thing live, which how to deploy an app built with AI walks through.

Frequently asked questions

How do I validate file uploads in an AI-built app?

Check both file type and size on the server, not just in the browser's file input. On Supabase Storage, set allowedMimeTypes and fileSizeLimit at the bucket level so the platform rejects bad uploads before your code runs. On S3, validate before generating the presigned URL and lock the ContentType into the signed request so it can't be swapped.

Is Supabase Storage secure enough for user file uploads?

Yes, if you configure it correctly. Supabase Storage blocks all uploads by default until you write row level security policies on the storage.objects table. The common mistake is skipping that step or writing a policy that doesn't scope files to the uploading user's own folder.

What's the difference between S3 presigned URLs and Supabase Storage for file uploads?

A presigned URL is a temporary, permission-scoped link you generate on your server so a client can upload directly to S3 without AWS credentials. Supabase Storage is a full storage service built on top of Postgres row level security, with bucket-level MIME and size limits and resumable uploads built in. Pick based on what the rest of your stack already runs on.

Do I need to scan uploaded files for malware?

If users can upload files that other people (staff or customers) will later open or your server will process, yes. Neither Supabase Storage nor raw S3 scans files for malware automatically. On AWS you can add a ClamAV Lambda triggered on upload or enable Amazon GuardDuty Malware Protection for S3, which scans new objects and can tag or quarantine infected ones.

Why does AI-generated file upload code often have security gaps?

AI coding tools optimize for a working demo, not a hostile input. They typically skip server-side type and size checks, write every file to one shared, unscoped bucket path, and never add a malware scan step, because none of those show up as an error during a quick test with a single trusted user.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.