9.5 KiB
Noah Bianchi Studio
Noah Bianchi is an original photographer client-delivery platform inspired by the category of premium gallery products such as DLVRD. It is built around one loop:
Upload -> Curate -> Customize -> Publish -> Send a private link
Photographers get a focused studio dashboard. Clients get a private editorial gallery without creating an account.
Stack
- Frontend: React, TypeScript, Vite, React Router, Tailwind CSS, Framer Motion
- Backend: Go 1.24+, Gin, REST/JSON,
database/sql - Database: PostgreSQL by default; SQLite remains available for lightweight local testing
- Storage: MinIO locally through an S3-compatible storage interface
- Media: direct browser uploads with presigned URLs, asynchronous preview processing, signed download URLs
Run Locally
Prerequisites:
- Docker Desktop with the Compose plugin
- Go 1.24 or newer
- Node.js 20 or newer
- GNU Make, or use the direct commands below
From the repository root:
cp .env.example .env
npm --prefix frontend install
docker compose up -d --wait
make migrate
make seed
Start the API:
make dev
Start the frontend in a second terminal:
make frontend-dev
Open:
- Dashboard: http://localhost:5173/login
- Dev diagnostics: http://localhost:5173/dashboard/dev
- Demo client gallery: http://localhost:5173/g/emma-james-wedding
- API documentation: http://localhost:8080/swagger/index.html
- MinIO console: http://localhost:9001
PowerShell
Copy-Item .env.example .env
npm --prefix frontend install
docker compose up -d --wait
go -C backend run ./cmd/migrate -dir ../migrations
go -C backend run ./cmd/seed
In separate terminals:
go -C backend run ./cmd/server
npm --prefix frontend run dev
Demo Credentials
The seed command creates:
Email: demo@example.com
Password: DemoPassword123!
Studio: Noah Bianchi
It also creates the published demo gallery:
Title: Emma & James
Slug: emma-james-wedding
Client: Emma & James
The demo media uses external placeholder URLs so the gallery is visually useful immediately. Real uploads use MinIO/S3 objects and never store media bytes in PostgreSQL.
If an upload stops at 0%, open Dashboard -> Dev. The workbench shows the effective browser origin, allowed CORS origins, MinIO reachability, bucket, and the latest client-side upload error.
Storage Configuration
The default .env.example uses MinIO:
STORAGE_ENDPOINT=localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin
STORAGE_BUCKET=gallery-media
STORAGE_USE_SSL=false
The Go storage.Storage interface supports:
CreateUploadURLCreateDownloadURLDeleteStatGetPut
The current implementation is MinIO, using the MinIO Go SDK. Replacing it with AWS S3, Cloudflare R2, or Backblaze B2 only requires another implementation of that interface and provider configuration.
Upload flow:
Browser -> POST /api/galleries/:id/uploads
API -> creates media row + presigned PUT URL
Browser -> PUT file directly to MinIO
Browser -> POST /api/uploads/:id/complete
Worker -> generates image previews and marks media READY
Original object keys are private. The API exposes only short-lived signed URLs to authenticated owners or authorized gallery visitors.
SQLite Mode
SQLite is retained as a development/test option. It still requires MinIO for the complete upload workflow, but it does not require PostgreSQL.
DB_DRIVER=sqlite
SQLITE_PATH=./data/surprise.db
Then run the same migration and seed commands:
make migrate
make seed
make dev
PowerShell process variables use $env: syntax:
$env:DB_DRIVER = "sqlite"
$env:SQLITE_PATH = ".\data\surprise.db"
go -C backend run ./cmd/migrate -dir ../migrations
go -C backend run ./cmd/seed
go -C backend run ./cmd/server
The migration runner selects migrations/sqlite/ for SQLite and the root migration files for PostgreSQL.
Product Workflow
Photographer
- Register or sign in.
- Create a gallery with title, client, and description.
- Drag in photos or videos.
- Uploads go directly to MinIO using presigned URLs with per-file progress.
- Set a cover image, ordering, layout, appearance, branding, password, expiry, and client controls.
- Preview the exact public renderer.
- Publish and copy
/g/:slug.
Client
- Open the private gallery link.
- Enter a gallery password if required.
- Browse responsive image and video media.
- Open photos in an immersive fullscreen viewer.
- Favorite photographs anonymously.
- Download individual originals or request an asynchronous gallery ZIP.
API Overview
Authentication:
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
PATCH /api/auth/me
POST /api/auth/password
Authenticated galleries:
GET /api/galleries
POST /api/galleries
GET /api/galleries/:id
PATCH /api/galleries/:id
DELETE /api/galleries/:id
POST /api/galleries/:id/publish
POST /api/galleries/:id/unpublish
GET /api/galleries/:id/preview
Uploads and media:
GET /api/galleries/:id/media
POST /api/galleries/:id/uploads
POST /api/uploads/:id/complete
PATCH /api/media/:id
POST /api/media/:id/download
DELETE /api/media/:id
DELETE /api/uploads/:id
Public gallery:
GET /api/public/galleries/:slug
POST /api/public/galleries/:slug/authenticate
POST /api/public/galleries/:slug/media/:mediaId/favorite
DELETE /api/public/galleries/:slug/media/:mediaId/favorite
POST /api/public/galleries/:slug/media/:mediaId/download
POST /api/public/galleries/:slug/download-all
GET /api/public/galleries/:slug/download-all/:jobId
Health:
GET /health
Swagger UI is available at /swagger/index.html; the generated Swagger document is also served at /swagger/doc.json. Regenerate the checked-in documentation after changing API annotations with:
make swagger
Photographer routes use an HTTP-only signed session cookie. Public visitor identity and gallery access are separate signed cookies; clients do not need accounts.
Database
The gallery platform migration adds:
users: photographer accounts and bcrypt password hashesgalleries: ownership, slug, publication state, controls, expiry, theme, and branding JSONmedia: original object metadata, preview keys, processing state, dimensions, and orderingfavorites: anonymous gallery-scoped favorite recordsdownloads: individual download audit recordsdownload_jobs: asynchronous ZIP status and object key
Migration files:
migrations/003_gallery_platform.sqlfor PostgreSQLmigrations/sqlite/003_gallery_platform.sqlfor SQLite
The schema does not store uploaded media content. external_url exists only to make the development seed gallery useful without shipping copyrighted or binary assets; real uploads have private storage_key values instead.
Project Structure
backend/
cmd/server/ Gin API router and worker startup
cmd/migrate/ dialect-aware SQL migration runner
cmd/seed/ demo photographer and gallery seed
internal/auth/ bcrypt auth, signed sessions, visitor access cookies
internal/galleries/ gallery CRUD and public gallery responses
internal/media/ upload lifecycle, media repository, preview worker
internal/storage/ MinIO/S3-compatible storage interface and adapter
internal/downloads async individual/ZIP download handling
frontend/
src/components/dashboard/ photographer workspace, editor, upload queue
src/components/gallery/ shared public/preview renderer and viewer
src/features/auth/ authenticated session context
src/pages/ auth, dashboard, editor, public gallery routes
src/lib/ typed API client and formatting helpers
migrations/ PostgreSQL and SQLite SQL migrations
docker-compose.yml PostgreSQL and MinIO
Preview and public delivery use the same ClientGallery component. The preview endpoint changes only authorization and the preview banner; it does not create a second mock gallery UI.
Checks
Backend:
make test
make lint
Frontend:
npm --prefix frontend run build
npm --prefix frontend run lint
npm --prefix frontend run format:check
The backend test suite includes real SQLite migration/repository/auth coverage. A live PostgreSQL/MinIO run requires Docker and should be performed with the clean-checkout commands above.
Deliberate MVP Boundaries
Implemented:
- Photographer registration/login/logout
- Gallery CRUD and ownership checks
- Gallery publication and preview
- MinIO presigned direct uploads
- Per-file upload progress and cancellation
- Image preview/thumbnail worker
- Video playback using optimized/original object URLs
- Gallery themes, layouts, branding, controls, password, expiry, and cover selection
- Public responsive gallery at
/g/:slug - Fullscreen photo viewer with keyboard and touch navigation
- Anonymous favorites
- Signed individual downloads
- Background ZIP generation and polling
Not implemented yet:
- Billing, subscriptions, teams, organizations, and CRM
- Persistent distributed job queue
- FFmpeg video transcoding and streaming manifests
- Full HEIC decoding; unsupported image formats fall back to the original object
- Production email delivery
- Advanced proofing/comments/analytics
- Custom domains and cloud archive workflows