SupaLaunch is a SaaS boilerplate built using Supabase and Next.js. It includes authentication, Stripe payments, Postgres database, 20+ TailwindCSS themes, emails, OpenAI API streaming, file storage and more.
File uploads are a crucial feature for many web applications, from profile pictures to document management systems. In this guide, we'll explore how to implement secure file uploads in a Next.js application using Supabase Storage.
Before diving in, make sure you have:
First, create a new project in Supabase from your dashboard if you haven't already.
Navigate to the Storage section in your Supabase dashboard and create a new bucket. Let's call it documents
.
By default, all buckets in Supabase are private. You'll need to set up RLS policies to control access.
To allow uploads to your bucket, add an RLS policy:
CREATE POLICY "Allow uploads" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'documents' AND auth.role() = 'anon'
);
For authenticated users only:
CREATE POLICY "Allow authenticated uploads" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'documents' AND auth.role() = 'authenticated'
);
Add the Supabase client to your Next.js project:
npm install @supabase/supabase-js
# or
yarn add @supabase/supabase-js
For apps using Next.js with authentication:
npm install @supabase/auth-helpers-nextjs
# or
yarn add @supabase/auth-helpers-nextjs
Create a .env.local
file in your project root and add:
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Create a utility file to initialize your Supabase client:
// lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseKey)
Here's a simple component that handles file uploads:
// components/FileUpload.jsx
import { useState } from 'react'
import { supabase } from '../lib/supabaseClient'
export default function FileUpload() {
const [uploading, setUploading] = useState(false)
const [filePath, setFilePath] = useState(null)
const [error, setError] = useState(null)
const handleFileChange = async (event) => {
try {
setUploading(true)
setError(null)
const file = event.target.files[0]
if (!file) return
// Upload file to Supabase
const { data, error } = await supabase.storage
.from('documents')
.upload(`public/${Date.now()}-${file.name}`, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
setFilePath(data.path)
alert('File uploaded successfully!')
} catch (error) {
setError(error.message)
alert('Error uploading file: ' + error.message)
} finally {
setUploading(false)
}
}
return (
<div>
<h2>Upload File</h2>
<input
type="file"
disabled={uploading}
onChange={handleFileChange}
/>
{uploading && <p>Uploading...</p>}
{error && <p className="error">Error: {error}</p>}
{filePath && <p>File uploaded to: {filePath}</p>}
</div>
)
}
// pages/upload.js
import FileUpload from '../components/FileUpload'
export default function UploadPage() {
return (
<div className="container">
<h1>File Upload Example</h1>
<FileUpload />
</div>
)
}
Add file type validation to ensure only desired file types are uploaded:
const handleFileChange = async (event) => {
try {
const file = event.target.files[0]
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (!allowedTypes.includes(file.type)) {
throw new Error('File type not supported. Please upload JPEG, PNG, or PDF.')
}
// Continue with upload...
} catch (error) {
setError(error.message)
}
}
Prevent uploading large files:
// Check file size (limit to 5MB)
if (file.size > 5 * 1024 * 1024) {
throw new Error('File size exceeds 5MB limit.')
}
For a better user experience, add a progress indicator:
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabaseClient'
export default function FileUploadWithProgress() {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const handleFileChange = async (event) => {
try {
const file = event.target.files[0]
setUploading(true)
setProgress(0)
// Simulate progress (Supabase doesn't provide upload progress)
const interval = setInterval(() => {
setProgress((prevProgress) => {
if (prevProgress >= 95) {
clearInterval(interval)
return prevProgress
}
return prevProgress + 5
})
}, 100)
const { data, error } = await supabase.storage
.from('documents')
.upload(`public/${Date.now()}-${file.name}`, file)
clearInterval(interval)
if (error) throw error
setProgress(100)
// Handle success
} catch (error) {
// Handle error
} finally {
setUploading(false)
}
}
return (
<div>
<input type="file" onChange={handleFileChange} disabled={uploading} />
{uploading && (
<div className="progress-bar">
<div className="progress" style={{ width: `${progress}%` }}></div>
<span>{progress}%</span>
</div>
)}
</div>
)
}
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabaseClient'
export default function FileDisplay({ filePath }) {
const [fileUrl, setFileUrl] = useState(null)
useEffect(() => {
if (filePath) {
const { data } = supabase.storage
.from('documents')
.getPublicUrl(filePath)
setFileUrl(data.publicUrl)
}
}, [filePath])
return fileUrl ? (
<div>
<h3>Uploaded File</h3>
{fileUrl.match(/\.(jpeg|jpg|gif|png)$/) ? (
<img src={fileUrl} alt="Uploaded file" style={{ maxWidth: '100%' }} />
) : (
<a href={fileUrl} target="_blank" rel="noopener noreferrer">
View File
</a>
)}
</div>
) : null
}
For private files, create temporary signed URLs:
const getSignedUrl = async (filePath) => {
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl(filePath, 60) // URL valid for 60 seconds
if (error) {
console.error('Error creating signed URL:', error)
return null
}
return data.signedUrl
}
For Next.js 13+ with the App Router, create a client component:
// app/components/FileUploader.jsx
'use client'
import { useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
export default function FileUploader() {
const [uploading, setUploading] = useState(false)
const supabase = createClientComponentClient()
const handleUpload = async (event) => {
try {
setUploading(true)
const file = event.target.files[0]
const { data, error } = await supabase.storage
.from('documents')
.upload(`public/${Date.now()}-${file.name}`, file)
if (error) throw error
// Handle success
} catch (error) {
// Handle error
} finally {
setUploading(false)
}
}
return (
<input type="file" onChange={handleUpload} disabled={uploading} />
)
}
Handle multiple file uploads with this example:
import { useState } from 'react'
import { supabase } from '../lib/supabaseClient'
export default function MultiFileUpload() {
const [uploading, setUploading] = useState(false)
const [uploadedFiles, setUploadedFiles] = useState([])
const handleMultipleFiles = async (event) => {
try {
setUploading(true)
const files = Array.from(event.target.files)
const uploads = files.map(async (file) => {
const { data, error } = await supabase.storage
.from('documents')
.upload(`public/${Date.now()}-${file.name}`, file)
if (error) throw error
return data.path
})
const results = await Promise.all(uploads)
setUploadedFiles(results)
} catch (error) {
console.error('Error uploading files:', error)
} finally {
setUploading(false)
}
}
return (
<div>
<input
type="file"
multiple
onChange={handleMultipleFiles}
disabled={uploading}
/>
{uploading && <p>Uploading multiple files...</p>}
{uploadedFiles.length > 0 && (
<div>
<h3>Uploaded Files:</h3>
<ul>
{uploadedFiles.map((path, index) => (
<li key={index}>{path}</li>
))}
</ul>
</div>
)}
</div>
)
}
If you encounter CORS errors, ensure your Supabase project has the correct CORS configuration. Go to the Auth settings in your Supabase dashboard and add your domain to the list of allowed domains.
Supabase has a default file size limit of 50MB. For larger files, consider splitting them into chunks or using a different solution.
Always validate file types on both client and server sides for security:
// Client-side validation
const isValidFileType = (file) => {
const validTypes = ['image/jpeg', 'image/png', 'application/pdf']
return validTypes.includes(file.type)
}
Implementing file uploads with Next.js and Supabase Storage is straightforward and secure. With the examples provided in this guide, you can quickly add file upload functionality to your applications while maintaining control over access permissions.
For more complex scenarios, consider:
The combination of Next.js and Supabase provides a powerful foundation for building robust file upload solutions for your web applications.
Happy coding!