How to enforce server-side file validation when using Supabase Storage client-side upload in Next.js?

1 day ago 2
ARTICLE AD BOX

I have a Next.js app (App Router) with two separate upload paths for a gallery feature, and I realized my server-side security checks are being bypassed.

The secure path (Server Action) - validates magic bytes and enforces 10MB limit:

// app/actions/upload-gallery-image.ts "use server"; function detectImageType(bytes: Uint8Array): string | null { if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) return "image/jpeg"; if (bytes[0] === 0x89 && bytes[1] === 0x50) return "image/png"; // ... } export async function uploadGalleryImage(formData: FormData) { const file = formData.get("file") as File; if (file.size > 10 * 1024 * 1024) return { error: "10MB limit" }; const bytes = new Uint8Array(await file.arrayBuffer()); if (!detectImageType(bytes)) return { error: "Invalid file type" }; await supabase.storage.from("gallery").upload(path, file); }

The actual path used in the client component — uploads directly to Supabase Storage, bypassing all the above:

// app/(dashboard)/gallery/GalleryPageClient.tsx "use client"; async function handleUpload() { for (const file of selectedFiles) { if (file.size > 50 * 1024 * 1024) { // only client-side check, 50MB not 10MB alert("too large"); continue; } // Uploads directly — no magic byte check, no server validation const { error } = await supabase.storage .from("gallery") .upload(storagePath, file, { contentType: file.type }); } }

The client component uses the Supabase JS SDK directly (initialized with the anon key), so it completely skips the Server Action that has the proper security logic.

What I want:

All uploads to go through the Server Action so magic byte validation and file size limits are always enforced Support for multi-file upload with a progress indicator per file Keep the UX responsive (not freeze the UI while uploading)

What I've considered:

Calling the Server Action in a loop for each file — but FormData can only hold one file per key, and Server Actions can't return upload progress Using an API Route (/api/upload) instead — but then I lose the simplicity of Server Actions Enforcing limits via Supabase Storage bucket policy — but that only covers size, not magic bytes

Question:
What is the recommended pattern in Next.js App Router to handle multi-file uploads that must pass server-side validation (magic bytes, file type), while keeping the client UI responsive? Should I move to an API Route, or is there a way to make Server Actions work here?

Environment:

Next.js 15 (App Router) @supabase/ssr 0.5.2 @supabase/supabase-js 2.49.4
Read Entire Article