A production-ready, single-page file sharing application built with Next.js 15, TypeScript, and Tailwind CSS. Upload files anonymously, generate a unique code, and share with others. Files automatically expire and delete.
✨ Key Features:
- 🔼 Anonymous file upload with drag & drop
- 📤 Upload progress tracking with visual progress bar
- 🎯 Generate unique 6-character codes for sharing
- 📱 QR code generation for easy mobile sharing
- ⏰ Configurable expiry times (15m, 1h, 24h)
- 🔗 Copy-to-clipboard functionality
- 🌓 Dark/light mode with localStorage persistence
- 📦 Cloudflare R2 integration for file storage
- 🗄️ Supabase for metadata storage
- 🚀 Auto-delete expired files (cron-ready)
- 📥 Download files by entering code
- 📊 File size validation (max 3GB)
- ✅ File type validation
- 🎨 Clean, minimal UI
- 📱 Fully responsive (mobile, tablet, desktop)
- Frontend: Next.js 15 (App Router), React 19, TypeScript
- Styling: Tailwind CSS (dark mode support)
- Icons: Lucide React
- Forms: React Dropzone, Axios
- QR Codes: qrcode.react
- State: Zustand (for optional future enhancements)
- Validation: Zod
- Database: Supabase (PostgreSQL)
- File Storage: Cloudflare R2 (S3-compatible)
- Type Checking: TypeScript 5.3
- Linting: ESLint
share-files/
├── app/
│ ├── api/
│ │ ├── upload/initiate/route.ts # Presigned upload initialization
│ │ ├── upload/complete/route.ts # Upload finalization + metadata save
│ │ ├── get-file/route.ts # File retrieval by code
│ │ └── delete-expired/route.ts # Cleanup endpoint
│ ├── components/
│ │ ├── Header.tsx # App header with theme toggle
│ │ ├── ThemeToggle.tsx # Dark/light mode toggle
│ │ ├── UploadBox.tsx # Drag-drop file upload
│ │ ├── ExpirySelector.tsx # Expiry time selector
│ │ ├── ProgressBar.tsx # Upload progress display
│ │ ├── ResultCard.tsx # Success state with code
│ │ ├── QRCodeDisplay.tsx # QR code generation
│ │ ├── DownloadForm.tsx # Download by code form
│ │ ├── HowItWorks.tsx # 3-step guide
│ │ └── Footer.tsx # Footer with branding
│ ├── lib/
│ │ ├── types.ts # TypeScript type definitions
│ │ ├── constants.ts # App constants
│ │ ├── codeGenerator.ts # Code generation logic
│ │ ├── validation.ts # Zod validation schemas
│ │ ├── supabaseClient.ts # Supabase client setup
│ │ ├── r2Client.ts # Cloudflare R2 client
│ │ └── utils.ts # Utility functions
│ ├── styles/
│ │ └── globals.css # Global Tailwind styles
│ ├── layout.tsx # Root layout
│ └── page.tsx # Main page (single-page app)
├── public/ # Static assets
├── .env.local # Environment variables (template)
├── .eslintrc.json # ESLint configuration
├── tailwind.config.ts # Tailwind configuration
├── tsconfig.json # TypeScript configuration
├── next.config.js # Next.js configuration
├── postcss.config.js # PostCSS configuration
├── package.json # Dependencies
└── README.md # This file
- Node.js 18+ and npm 9+
- Supabase account (free tier available)
- Cloudflare R2 account (free 10GB/month)
npm install --legacy-peer-depsThe --legacy-peer-deps flag is needed due to qrcode.react's React version constraints.
Copy the .env.local template and fill in your credentials:
cp .env.local.example .env.localThen edit .env.local with:
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Cloudflare R2 Configuration
R2_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-access-key-id
R2_SECRET_ACCESS_KEY=your-secret-access-key
R2_BUCKET_NAME=share-files
R2_REGION=auto
# Cleanup API Secret (for cron jobs)
CLEANUP_API_SECRET=your-cleanup-secret- Create a Supabase project
- Run the SQL schema to create the
filestable:
-- Create files table
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(6) UNIQUE NOT NULL,
file_url TEXT NOT NULL,
filename VARCHAR(255) NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100),
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
downloaded_count INT DEFAULT 0
);
-- Create indexes for performance
CREATE INDEX idx_code ON files(code);
CREATE INDEX idx_expires_at ON files(expires_at);
-- Enable Row Level Security (optional but recommended)
ALTER TABLE files ENABLE ROW LEVEL SECURITY;
-- Create policy for anonymous access
CREATE POLICY "Allow anonymous access" ON files
FOR SELECT USING (true);- Get your API credentials from Supabase Dashboard:
Settings → API→Project URL→NEXT_PUBLIC_SUPABASE_URLSettings → API→Service Role Secret Key→SUPABASE_SERVICE_ROLE_KEY
- Create a Cloudflare account
- Go to
R2→Create bucket→ name itshare-files - Create API token:
Account Home → R2 → SettingsCreate API tokenwith read/write permissions
- Get credentials:
R2_ACCOUNT_ID: Your Cloudflare Account IDR2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY: From the API tokenR2_BUCKET_NAME:share-files(or your bucket name)
npm run devOpen http://localhost:3000 in your browser.
npm run build
npm startInitialize an upload and receive a presigned direct-upload URL.
Request:
Content-Type: application/json
{
"files": [{ "name": "file.mov", "size": 1234, "type": "video/quicktime", "path": "folder/file.mov" }],
"expiryMinutes": 60
}
Response (200):
{
"code": "ABCDEF",
"uploadUrl": "https://...",
"fileKey": "ABCDEF/sharefiles-ABCDEF.zip",
"filename": "sharefiles-ABCDEF.zip",
"mimeType": "application/zip",
"fileCount": 2,
"maxUploadSizeBytes": 1073741824
}Finalize an upload after the browser PUTs directly to R2.
Request:
{
"code": "ABCDEF",
"fileKey": "ABCDEF/sharefiles-ABCDEF.zip",
"filename": "sharefiles-ABCDEF.zip",
"mimeType": "application/zip",
"fileSize": 1048576,
"fileCount": 2,
"expiryMinutes": 60
}Response (201):
{
"code": "ABCDEF",
"expiresAt": "2025-03-26T06:30:00Z",
"expiresIn": 60,
"fileSize": 1048576,
"filename": "sharefiles-ABCDEF.zip",
"fileCount": 2
}Error (400/413/500):
{
"code": "FILE_TOO_LARGE" | "INVALID_FILE_TYPE" | "UPLOAD_FAILED" | "NO_FILE",
"message": "Human-readable error message"
}Retrieve file metadata and download URL.
Response (200):
{
"fileUrl": "https://...",
"filename": "document.pdf",
"fileSize": 1048576,
"expiresAt": "2025-03-26T06:30:00Z",
"expiresIn": 45
}Error (404/410):
{
"code": "INVALID_CODE" | "EXPIRED",
"message": "File not found or has expired"
}Clean up expired files and orphan R2 uploads (requires Bearer token).
Request:
Authorization: Bearer <CLEANUP_API_SECRET>
Response (200):
{
"message": "Cleanup completed. Deleted 5 expired files and 2 orphan objects.",
"deleted_count": 5,
"orphan_deleted_count": 2,
"scanned_objects": 123,
"status": "success"
}Files are automatically scheduled for deletion when they expire. To set up automatic cleanup:
This repo includes vercel.json with a cron job that calls /api/delete-expired once daily.
Set both environment variables in your Vercel project:
CLEANUP_API_SECRET=your-strong-secret
CRON_SECRET=your-strong-secretCRON_SECRET is used by Vercel when invoking cron routes, and your API route verifies the Bearer token against CLEANUP_API_SECRET.
Create a scheduled Cloudflare Worker:
export default {
async scheduled(event, env, ctx) {
const secret = env.CLEANUP_API_SECRET;
const url = 'https://your-domain.com/api/delete-expired';
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${secret}`,
},
});
const result = await response.json();
console.log('Cleanup result:', result);
},
};Schedule it to run every 5 minutes or hourly.
Services like EasyCron can trigger:
POST https://your-domain.com/api/delete-expired
Header: Authorization: Bearer YOUR_CLEANUP_API_SECRET
Codes are 6 characters using safe alphanumeric characters:
- Includes:
A-Z, 2-9 - Excludes:
I, L, O, 0, 1(confusing characters) - Format:
ABCDEF(uppercase, no special chars)
Supported MIME types include:
- Documents: PDF, Word, Excel, PowerPoint, Plain text, CSV
- Archives: ZIP, RAR, 7Z, GZIP
- Images: JPEG, PNG, GIF, WebP, SVG
- Audio: MP3, WAV, MP4, WebM
- Video: MP4, WebM, MOV, AVI
Max file size: 1GB
Dark mode preference is automatically detected from system settings and can be toggled via the theme button in the header. Preference is persisted in localStorage.
- ✅ Static pre-rendering of main page
- ✅ API route compression
- ✅ Image optimization with Next.js Image component
- ✅ CSS optimization via Tailwind
- ✅ Bundle size optimization (132KB First Load JS)
- ✅ Database indexes on frequently queried fields
- File size validation (client + server)
- File type validation
- Expiry time validation
- Unique code generation with collision prevention
- Graceful error messages for users
- Comprehensive logging for debugging
- Password protection for downloads
- Download tracking and analytics
- Custom expiry duration input
- Email notifications
- Download link encryption
- Rate limiting by IP
- Bandwidth usage tracking
- Custom domain CNAME support
- File preview before download
- Batch file uploads
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Mobile browsers (iOS Safari, Chrome Mobile)
- Anonymous: No user accounts or tracking
- Encrypted: Files transmitted over HTTPS
- Auto-delete: Automatic expiration and deletion
- No logging: No download logs or IP logging
- No ads: Clean, ad-free interface
- Open source: Full code transparency
- Push to GitHub
- Connect to Vercel
- Add environment variables in Vercel dashboard
- Deploy with one click
FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --legacy-peer-deps
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]npm install --legacy-peer-deps
npm run build
npm startUse npm install --legacy-peer-deps
- Verify
NEXT_PUBLIC_SUPABASE_URLis set - Check service role key has admin access
- Ensure
filestable exists with correct schema
- Verify
R2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY - Check bucket name matches
R2_BUCKET_NAME - Ensure API token has read/write permissions
- Ensure
qrcode.reactand types are installed - Clear
.nextfolder and rebuild
- Check browser localStorage permissions
- Clear cookies/cache and reload
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Commit changes with clear messages
- Submit a pull request
MIT License - See LICENSE.md for details
Built with ❤️ using Next.js, TypeScript, and Tailwind CSS
Happy file sharing! 🎉