A modern photography portfolio website built with Next.js 16, featuring interactive maps, photo management, and a comprehensive dashboard.
- Next.js 16 with React 19 and React Compiler
- TanStack Query v5 for advanced data fetching and caching
- tRPC v11 for end-to-end type-safe APIs
- Interactive Maps with Mapbox GL JS integration
- Photo Management with EXIF data extraction and iPhone album integration
- Real-time Dashboard with analytics and statistics
- Modern UI built with Tailwind CSS and shadcn/ui components
- Authentication powered by Better Auth
- Database using Drizzle ORM with PostgreSQL
- File Storage via S3-compatible storage
Before deploying, ensure you have:
- Node.js 18+ or Bun runtime
- PostgreSQL database (recommended: Neon, Supabase, or Vercel Postgres)
- S3-compatible storage for image storage (AWS S3, Cloudflare R2, DigitalOcean Spaces, etc.)
- Mapbox account for map features
- Vercel account for deployment (or any Node.js hosting provider)
All personal branding is centralized in one file: src/site.config.ts. Edit it to make the site your own:
| Field | Description |
|---|---|
name |
Your name (logo, profile cards, footer) |
tagline |
Short tagline next to logo (e.g. "Photo") |
role |
Your title (e.g. "Photographer") |
bio |
Short bio on the home page |
avatar |
Avatar image path (place file in /public/avatar.jpg) |
initials |
Fallback text when avatar fails to load |
metadata |
SEO title & description |
socialLinks |
Social links with icons (Instagram, GitHub, X, Xiaohongshu, Contact me) |
footer |
Footer attribution credits |
mapbox |
Custom Mapbox style URLs for light/dark themes |
imageLoader |
Set to "cloudflare" or "default" based on your storage provider |
gear |
Camera gear shown on the About page |
Quick start:
# 1. Edit the config
vim src/site.config.ts
# 2. Replace avatar
cp your-photo.jpg public/avatar.jpg
# 3. Replace about background
cp your-bg.jpg public/bg.jpgNote: If you are NOT using Cloudflare R2, set
imageLoaderto"default"insite.config.ts. This replaces the old manual edit ofnext.config.ts.
# Clone the repository
git clone https://github.com/ECarry/photography-website.git
cd photography-website
# Install dependencies
bun install
# or
npm installCreate a .env file in the root directory:
cp .env.example .envConfigure the following environment variables:
# PostgreSQL connection string
DATABASE_URL=postgresql://username:password@host:port/database_name?sslmode=requireFor Neon Database:
- Create account at neon.tech
- Create a new project
- Copy the connection string from dashboard
For Supabase:
- Create project at supabase.com
- Go to Settings > Database
- Copy the connection string
# Generate a random secret key (32+ characters)
BETTER_AUTH_SECRET=your-super-secret-key-here
# Your app's base URL
BETTER_AUTH_URL=https://your-domain.com
NEXT_PUBLIC_APP_URL=https://your-domain.com# S3-compatible storage settings
S3_ENDPOINT=https://your-s3-endpoint.com
S3_BUCKET_NAME=your-bucket-name
S3_PUBLIC_URL=https://your-custom-domain.com
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
NEXT_PUBLIC_S3_PUBLIC_URL=https://your-custom-domain.comSupported Storage Providers:
Cloudflare R2:
S3_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com
S3_BUCKET_NAME=your-bucket-name
NEXT_PUBLIC_S3_PUBLIC_URL=https://your-custom-domain.comAWS S3:
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET_NAME=your-aws-bucket
NEXT_PUBLIC_S3_PUBLIC_URL=https://your-bucket.s3.amazonaws.comDigitalOcean Spaces:
S3_ENDPOINT=https://nyc3.digitaloceanspaces.com
S3_BUCKET_NAME=your-space-name
NEXT_PUBLIC_S3_PUBLIC_URL=https://your-space.nyc3.digitaloceanspaces.comMinIO (Self-hosted):
S3_ENDPOINT=http://localhost:9000
S3_BUCKET_NAME=your-minio-bucket
NEXT_PUBLIC_S3_PUBLIC_URL=http://localhost:9000/your-bucketWasabi:
S3_ENDPOINT=https://s3.wasabisys.com
S3_BUCKET_NAME=your-wasabi-bucket
NEXT_PUBLIC_S3_PUBLIC_URL=https://your-bucket.s3.wasabisys.comIf you are not using Cloudflare R2, set imageLoader to "default" in src/site.config.ts. This is handled automatically β no need to manually edit next.config.ts.
Setup Instructions:
- Choose your preferred S3-compatible storage provider
- Create an account and set up a bucket
- Generate API credentials (Access Key ID and Secret Access Key)
- Configure the endpoint URL for your provider
- (Optional) Setup custom domain for public access
# Mapbox access token
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=pk.your-mapbox-tokenGet Mapbox Token:
- Create account at mapbox.com
- Go to Account > Access Tokens
- Create a new token with appropriate scopes
# Default admin user for seeding
SEED_USER_EMAIL=admin@yourdomain.com
SEED_USER_PASSWORD=your-secure-password
SEED_USER_NAME=Admin User# Push database schema
bun run db:push
# Create admin user
bun run seed:user# Build the application
bun run build
# Test the production build locally
bun run startVisit http://localhost:3000 to verify everything works correctly.
# Install Vercel CLI
npm i -g vercel
# Login to Vercel
vercel login
# Deploy
vercel --prod- Push your code to GitHub/GitLab/Bitbucket
- Connect your repository to Vercel
- Configure environment variables in Vercel dashboard
- Deploy automatically on push
In your Vercel dashboard, add all environment variables from your .env file:
- Go to Project Settings > Environment Variables
- Add each variable with appropriate values for production
- Make sure to update URLs to use your production domain
# If you need to run migrations on production
vercel env pull .env.local
bun run db:push# Seed admin user in production
vercel env pull .env.local
bun run seed:user-
Change Summary
- The database
urlfield now stores the S3 object key (e.g.,photos/IMG_0001.jpg) instead of a full public URL. - Benefit: You can switch domains or CDNs freely by updating environment variables for the public base URL, without mass-updating the database.
- The database
-
Migration Steps (run before production, recommended)
- Backup your database (strongly recommended).
- Run the cleanup script to convert existing full URLs to S3 keys:
bun run clean:photo-urls
- Verify the result: spot-check several records;
urlshould look like a key such aspath/to/object.jpg. - If you need to rollback, run:
bun run rollback:photo-urls
-
Notes
- Ensure your public access domain is configured via
S3_PUBLIC_URLorNEXT_PUBLIC_S3_PUBLIC_URL. At runtime, the app combines this base URL with the key to form a full public URL. - If you have custom prefixes or multiple buckets, validate the script behavior in a staging environment first.
- Ensure your public access domain is configured via
This project supports two Docker deployment modes: Standalone (self-hosted) and Cloud (managed services).
Run the entire stack (App, PostgreSQL, RustFS) locally. Ideal for testing and self-hosting.
docker compose up -d
# or explicitly:
docker compose -f docker-compose.standalone.yml up -d- App: http://localhost:3000
- Postgres: localhost:5432
- RustFS (S3): localhost:9000
- RustFS Console: http://localhost:9001
Run only the App container, connecting to external services (e.g., Neon Postgres, AWS S3, Cloudflare R2).
- Create a
.envfile with your credentials:DATABASE_PROVIDER=cloud DATABASE_URL="postgres://..." BETTER_AUTH_SECRET="..." S3_ACCESS_KEY_ID="..." S3_SECRET_ACCESS_KEY="..." S3_BUCKET_NAME="..." S3_ENDPOINT="..." # Optional S3_PUBLIC_URL="..."
- Start the service:
docker compose -f docker-compose.cloud.yml up -d
The Dockerfile uses a multi-stage build with two deployment targets:
| Target | Used by | Strategy |
|---|---|---|
dev |
docker-compose.yml (Standalone) |
Runtime build β app is built inside the container at startup so SSG can access the local database |
runner |
docker-compose.cloud.yml (Cloud) |
Pre-built β app is built during docker build using Next.js standalone output for a smaller, faster image |
Standalone mode:
- First Start: Will take 1-2 minutes to compile.
- Restarts: Will be fast (cached via
next_cachevolume).
Cloud mode:
- Image is pre-built and optimized (~200MB vs ~1GB).
- Starts instantly β no build step at runtime.
- Environment variables for SSG are passed as build args.
-
Vercel Custom Domain:
- Go to Project Settings > Domains
- Add your custom domain
- Configure DNS records as instructed
-
Update Environment Variables:
BETTER_AUTH_URL=https://your-custom-domain.com NEXT_PUBLIC_APP_URL=https://your-custom-domain.com
-
Enable Vercel Analytics:
npm install @vercel/analytics
-
Configure Image Optimization:
- Ensure S3-compatible storage is properly configured
- Set up custom domain for your storage bucket
- Configure CDN settings (CloudFlare, AWS CloudFront, etc.)
-
Environment Variables:
- Never commit
.envfiles - Use strong, unique secrets
- Rotate keys regularly
- Never commit
-
Database Security:
- Use connection pooling
- Enable SSL connections
- Restrict database access by IP
-
File Upload Security:
- Configure proper CORS settings
- Implement file type validation
- Set upload size limits
The application is fully responsive and optimized for mobile devices:
- Progressive Web App features
- Touch-friendly interface
- Optimized images with lazy loading
- Fast loading with Next.js optimizations
# Clear Next.js cache
rm -rf .next
# Reinstall dependencies
rm -rf node_modules
bun install- Verify
DATABASE_URLis correct - Check database server status
- Ensure SSL settings match requirements
- Verify S3 storage credentials
- Check CORS settings on your storage bucket
- Ensure bucket permissions are correct
- Verify endpoint URL is correct for your provider
- Verify Mapbox token is valid
- Check token permissions and scopes
- Ensure domain is authorized in Mapbox settings
- Next.js Documentation
- Vercel Deployment Guide
- Drizzle ORM Documentation
- Better Auth Documentation
- AWS S3 Documentation
- Cloudflare R2 Documentation
- DigitalOcean Spaces Documentation
- Mapbox Documentation
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
If you find this project helpful, please give it a βοΈ on GitHub!
This project is licensed under the MIT License - see the LICENSE file for details.
Need help? Check the troubleshooting section above or open an issue in the repository.

