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
- Copy the block files into your project.
- Install peer dependencies:
npm install @supabase/supabase-js zod4. Environment
| Variable | Required | Default | Purpose |
|---|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | yes | — | Your Supabase project URL |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | yes | — | Anon key — used client-side to upload files |
| SUPABASE_SERVICE_ROLE_KEY | yes | — | Service role key — used server-side to generate signed URLs |
| UPLOAD_BUCKET | no | — | Storage bucket name. Defaults to uploads |
| MAX_FILE_SIZE_MB | no | — | Max file size in MB. Defaults to 10 |
| ALLOWED_FILE_TYPES | no | — | Comma-separated MIME types. Defaults to image/*, application/pdf |
5. Wire it in
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 })
}'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
- Drop a file onto the
FileUploadZoneand confirm the progress bar fills. - After upload, open Supabase Dashboard → Storage → your bucket and confirm the file appears.
- Call
getSignedUrl(path)server-side and confirm you can download the file via the returned URL. - Attempt to upload a file larger than
MAX_FILE_SIZE_MBand 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.