Utility · Starter

File Upload

Drag-and-drop file uploader — Supabase Storage with signed URLs, type and size validation, simulated progress bar, image previews, multi-file support, and a file-list component.

Read the Getting Access guideif you haven't yet.

1. Get the file

Sign in at marrowstack.dev, open the File Upload block, and click Copy all files. Paste lib/fileupload.ts (and the React components) into your project.

Before first use, create a storage bucket in Supabase Dashboard → Storage → New bucket. Set it to Private — the block uses signed URLs for all access rather than public bucket URLs.

2. Prerequisites

  • Next.js 14 or 15, App Router
  • Supabase project with a Storage bucket created
  • Tailwind CSS (the drop zone component uses Tailwind)

3. Install

  1. Copy the block files into your project.
  2. Install peer dependencies:
bash
npm install @supabase/supabase-js zod

4. Environment

VariableRequiredDefaultPurpose
NEXT_PUBLIC_SUPABASE_URLyesYour Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEYyesAnon key — used client-side to upload files
SUPABASE_SERVICE_ROLE_KEYyesService role key — used server-side to generate signed URLs
UPLOAD_BUCKETnoStorage bucket name. Defaults to uploads
MAX_FILE_SIZE_MBnoMax file size in MB. Defaults to 10
ALLOWED_FILE_TYPESnoComma-separated MIME types. Defaults to image/*, application/pdf

5. Wire it in

app/api/upload/signed-url/route.ts
import { getSignedUploadUrl } from '@/lib/fileupload'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions)
  if (!session?.user?.id) return new NextResponse('Unauthorized', { status: 401 })

  const { filename, contentType } = await req.json()
  const { signedUrl, path } = await getSignedUploadUrl({
    userId: session.user.id,
    filename,
    contentType,
  })

  return NextResponse.json({ signedUrl, path })
}
Client — drop zone component
'use client'
import { FileUploadZone, FileList } from '@/lib/fileupload'

export function AttachmentsSection() {
  return (
    <div>
      <FileUploadZone
        onUploaded={(files) => console.log('Uploaded:', files)}
        maxFiles={5}
        accept="image/*,application/pdf"
      />
      <FileList files={uploadedFiles} onDelete={handleDelete} />
    </div>
  )
}

6. Verify it works

  1. Drop a file onto the FileUploadZone and confirm the progress bar fills.
  2. After upload, open Supabase Dashboard → Storage → your bucket and confirm the file appears.
  3. Call getSignedUrl(path) server-side and confirm you can download the file via the returned URL.
  4. Attempt to upload a file larger than MAX_FILE_SIZE_MB and confirm the client-side error message appears.

7. Failure modes & fixes

Bucket not found

Cause: The storage bucket named in UPLOAD_BUCKET does not exist.

Fix: Create it in Supabase Dashboard → Storage → New bucket. Name must match exactly.

Invalid signature for signed URL

Cause: The signed URL was used after its expiry (default: 60 seconds for upload, 3600 for download).

Fix: Generate a fresh signed URL for each upload attempt. Don't cache upload URLs.

File type rejected

Cause: The uploaded file's MIME type is not in ALLOWED_FILE_TYPES.

Fix: Add the MIME type to ALLOWED_FILE_TYPES, or update the Zod schema in fileupload.ts.