Build a file sharing app with V0 using Next.js, Supabase Storage for uploads, and shareable links with expiry and password protection. You'll create a drag-and-drop uploader, link management with download limits, and access logging — all in about 1-2 hours without touching a terminal.
What you're building
File sharing is essential for teams, clients, and creative professionals who need to send large files securely. Instead of relying on email attachments or third-party services, a custom file sharing app gives you control over access, expiry, and branding.
V0 generates the upload interface, share link management, and download pages from prompts. Supabase Storage handles file hosting with automatic CDN delivery, and signed URLs with short TTLs ensure files are served securely without loading entire files into serverless function memory.
The architecture uses Next.js App Router with a client component for the interactive upload zone, Server Actions for share link management, API routes for download validation and redirection, and Supabase Storage for file hosting with RLS-protected access.
Final result
A WeTransfer-style file sharing app with drag-and-drop uploads, password-protected share links with expiry, download tracking, and a file management dashboard.
Tech stack
Prerequisites
- A V0 account (Premium plan for upload component iterations)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Understanding of file types you want to support (images, documents, archives)
- A use case: client deliverables, team file sharing, or public downloads
Build steps
Set up the project and file storage schema
Open V0, create a new project, and connect Supabase. Create the schema for files, share links, and download logs. Configure a Supabase Storage bucket for file uploads.
1// Paste this prompt into V0's AI chat:2// Build a file sharing app. Create a Supabase schema with:3// 1. files: id (uuid PK), owner_id (uuid FK to auth.users), filename (text), original_name (text), file_size (bigint), mime_type (text), storage_path (text), created_at (timestamptz)4// 2. share_links: id (uuid PK), file_id (uuid FK to files), token (text unique default gen_random_uuid()), password_hash (text), expires_at (timestamptz), max_downloads (int), download_count (int default 0), is_active (boolean default true), created_at (timestamptz)5// 3. download_logs: id (uuid PK), share_link_id (uuid FK to share_links), ip_address (text), user_agent (text), downloaded_at (timestamptz default now())6// Create a Supabase Storage bucket 'files' with 50MB file size limit.7// Add RLS: users see their own files, anyone with a valid share token can download.Pro tip: Configure the 'files' Storage bucket in Supabase Dashboard with RLS that allows authenticated uploads and restricts downloads to signed URL paths only.
Expected result: Database schema created with file tracking, share links, and download logs. Storage bucket configured for uploads.
Build the upload page with drag-and-drop
Create the main upload page with a drag-and-drop zone that supports multiple files. Each file shows an upload progress bar, and completed uploads appear in the file list below.
1// Paste this prompt into V0's AI chat:2// Build a file upload page at app/page.tsx with a 'use client' upload component.3// Requirements:4// - A large drag-and-drop zone that accepts any file type up to 50MB5// - Support multiple file selection (both drag-drop and click-to-browse)6// - Show each file being uploaded with: filename, file size, Progress bar, cancel Button7// - Upload each file to Supabase Storage bucket 'files' with path: {user_id}/{uuid}-{filename}8// - After upload, insert a record into the files table with storage_path, file_size, mime_type9// - Below the upload zone, show "My Files" as a Table with columns: filename (with file type icon), size (formatted), uploaded date, actions (share Button, delete Button)10// - The share Button opens a Dialog for creating a share link with:11// - DatePicker for expiry date (optional)12// - Switch for password protection with Input for password13// - Select for max downloads (unlimited, 1, 5, 10, 25)14// - "Create Link" Button that generates the share URL15// - Copy-to-clipboard Button for the generated link16// - Use Card to wrap the upload zone and Badge for file type indicatorsExpected result: Users can drag and drop files, see upload progress, and manage uploaded files with share link creation dialogs.
Create the download API route with validation
Build the download route that validates the share link token, checks expiry, password, and download limits, then redirects to a short-lived Supabase Storage signed URL.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { createHash } from 'crypto'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910export async function GET(11 req: NextRequest,12 { params }: { params: Promise<{ token: string }> }13) {14 const { token } = await params15 const password = req.nextUrl.searchParams.get('p')1617 const { data: link } = await supabase18 .from('share_links')19 .select('*, files(storage_path, original_name)')20 .eq('token', token)21 .eq('is_active', true)22 .single()2324 if (!link) {25 return NextResponse.json({ error: 'Link not found' }, { status: 404 })26 }2728 if (link.expires_at && new Date(link.expires_at) < new Date()) {29 return NextResponse.json({ error: 'Link expired' }, { status: 410 })30 }3132 if (link.max_downloads && link.download_count >= link.max_downloads) {33 return NextResponse.json({ error: 'Download limit reached' }, { status: 429 })34 }3536 if (link.password_hash) {37 const hash = createHash('sha256').update(password ?? '').digest('hex')38 if (hash !== link.password_hash) {39 return NextResponse.json({ error: 'Invalid password' }, { status: 401 })40 }41 }4243 const { data: signedUrl } = await supabase.storage44 .from('files')45 .createSignedUrl(link.files.storage_path, 30)4647 await supabase48 .from('share_links')49 .update({ download_count: link.download_count + 1 })50 .eq('id', link.id)5152 await supabase.from('download_logs').insert({53 share_link_id: link.id,54 ip_address: req.headers.get('x-forwarded-for') ?? 'unknown',55 user_agent: req.headers.get('user-agent') ?? 'unknown',56 })5758 return NextResponse.redirect(signedUrl!.signedUrl)59}Pro tip: Use a 30-second TTL on signed URLs so they expire quickly after the redirect. This prevents URL sharing — each download request generates a fresh signed URL.
Expected result: The download route validates all constraints (expiry, password, limit), logs the download, and redirects to a short-lived signed URL.
Build the public download page
Create the public-facing download page where recipients see file info and enter a password if required. The page fetches share link metadata server-side and renders the download interface.
1// Paste this prompt into V0's AI chat:2// Build a public download page at app/share/[token]/page.tsx.3// Requirements:4// - Server Component that fetches the share link with file info by token5// - Show a centered Card with: file icon based on mime_type, file name, file size formatted, expiry date if set6// - If the link is expired, show an error message: "This link has expired"7// - If download limit reached, show: "Download limit reached"8// - If password protected, show a 'use client' form with Input for password and "Download" Button9// - If no password, show a "Download" Button directly10// - The download Button links to /api/download/{token} (with password as query param if needed)11// - Show download count: "Downloaded X times" or "X of Y downloads used"12// - Add branding footer: "Shared via [Your App Name]"13// - Use Badge for file type, Card for the main container14// - Handle 404 if token doesn't exist with not-found.tsxExpected result: Recipients see file info on the download page. Password-protected files show an input field. Expired or used links show appropriate error messages.
Add download history and file management
Build the file detail page showing all share links and their download history. Users can create additional share links, revoke existing ones, and view download analytics.
1// Paste this prompt into V0's AI chat:2// Build a file detail page at app/files/[id]/page.tsx.3// Requirements:4// - Server Component that fetches the file with all share_links and their download_logs5// - File info Card: filename, size, mime_type, uploaded date, storage path6// - "Create Share Link" Button that opens a Dialog (same as upload page share dialog)7// - Active share links Table with columns: link URL (truncated with copy Button), expiry date, downloads used/max, password (yes/no Badge), created date, Revoke Button8// - Revoke sets is_active to false via Server Action9// - Download history Table below: download date, IP address (masked: 192.168.x.x), user agent (browser name), which share link was used10// - Stats Cards: total downloads across all links, active links count, last download date11// - Delete file Button with AlertDialog confirmation that removes from Storage and database12// - Use Tabs to switch between Share Links and Download History viewsExpected result: File detail shows all share links with download analytics. Users can create, revoke, and monitor share links.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { createHash } from 'crypto'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910export async function GET(11 req: NextRequest,12 { params }: { params: Promise<{ token: string }> }13) {14 const { token } = await params15 const password = req.nextUrl.searchParams.get('p')1617 const { data: link } = await supabase18 .from('share_links')19 .select('*, files(storage_path, original_name)')20 .eq('token', token)21 .eq('is_active', true)22 .single()2324 if (!link) {25 return NextResponse.json({ error: 'Not found' }, { status: 404 })26 }2728 if (link.expires_at && new Date(link.expires_at) < new Date()) {29 return NextResponse.json({ error: 'Expired' }, { status: 410 })30 }3132 if (link.max_downloads && link.download_count >= link.max_downloads) {33 return NextResponse.json({ error: 'Limit reached' }, { status: 429 })34 }3536 if (link.password_hash) {37 const hash = createHash('sha256').update(password ?? '').digest('hex')38 if (hash !== link.password_hash) {39 return NextResponse.json({ error: 'Wrong password' }, { status: 401 })40 }41 }4243 const { data } = await supabase.storage44 .from('files')45 .createSignedUrl(link.files.storage_path, 30)4647 await supabase48 .from('share_links')49 .update({ download_count: link.download_count + 1 })50 .eq('id', link.id)5152 await supabase.from('download_logs').insert({53 share_link_id: link.id,54 ip_address: req.headers.get('x-forwarded-for') ?? 'unknown',55 user_agent: req.headers.get('user-agent') ?? 'unknown',56 })5758 return NextResponse.redirect(data!.signedUrl)59}Customization ideas
Add file previews
Generate thumbnails for images and PDFs during upload. Show preview cards in the gallery view and on the download page.
Add folder organization
Create a folders table and let users organize files into nested folder structures with breadcrumb navigation.
Add team sharing
Invite team members who can access all files in a shared workspace, with role-based permissions for upload and delete.
Add storage usage tracking
Track total storage per user and show usage bars with limits. Implement quota enforcement in the upload API route.
Common pitfalls
Pitfall: Loading entire files into serverless function memory for downloads
How to avoid: Use Supabase Storage signed URLs with short TTL and redirect the user. The file streams directly from Supabase CDN, bypassing your function.
Pitfall: Not hashing passwords for share links
How to avoid: Hash passwords with SHA-256 before storing in password_hash. Compare hashes on download requests.
Pitfall: Using long-lived signed URLs for downloads
How to avoid: Use 30-second TTL on signed URLs generated fresh for each download request.
Best practices
- Use Supabase Storage signed URLs with short TTLs (30 seconds) to prevent URL sharing and enforce download limits.
- Hash share link passwords with SHA-256 before storing — never store plain text passwords.
- Redirect to signed URLs instead of streaming files through serverless functions to avoid memory limits.
- Log all downloads with IP and user agent for analytics and abuse detection.
- Configure Supabase Storage bucket with RLS that allows authenticated uploads and restricts reads to signed URLs.
- Use Design Mode (Option+D) to adjust the upload zone and file card layouts without spending V0 credits.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a file sharing app with Next.js and Supabase Storage. I need to create shareable download links with expiry dates, password protection, and download limits. Show me the database schema, the download API route that validates all constraints and redirects to a signed URL, and the public download page UI.
Build the share link creation Dialog for a file sharing app. When clicking Share on a file, open a Dialog with: DatePicker for optional expiry, Switch for password protection with Input, Select for max downloads (unlimited/1/5/10/25). On submit, insert into share_links and display the generated URL with a copy-to-clipboard Button.
Frequently asked questions
How do I handle large file uploads?
Supabase Storage supports files up to 50MB on the free tier and 5GB on paid plans. Files upload directly from the browser to Supabase Storage, bypassing your serverless function entirely.
Can I set download limits on shared links?
Yes. The share_links table has max_downloads and download_count columns. The download API route checks if download_count >= max_downloads before serving the file.
How do password-protected links work?
Passwords are hashed with SHA-256 and stored in the share_links table. The public download page shows a password Input. The hash is compared in the download API route before generating a signed URL.
How do I deploy this app?
Publish via V0's Share menu. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the Vars tab. Configure the Storage bucket in Supabase Dashboard with appropriate RLS policies.
Can files be shared without creating an account?
The uploader needs an account. Recipients can download without an account by visiting the share link URL. Password protection adds an extra layer of security.
Can RapidDev help build a custom file sharing platform?
Yes. RapidDev has built 600+ apps including enterprise file sharing systems with team workspaces, audit trails, and compliance features. Book a free consultation to discuss your needs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation