2026-08-22 21:25:53 +02:00
2026-08-22 21:25:53 +02:00
2026-08-22 20:58:27 +02:00
2026-08-22 17:27:55 +02:00
2026-08-22 21:21:30 +02:00
2026-08-22 20:58:27 +02:00
2026-08-22 02:59:16 +02:00
2026-08-22 21:21:30 +02:00
2026-08-22 17:27:55 +02:00
2026-08-22 17:27:55 +02:00

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:

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:

  • CreateUploadURL
  • CreateDownloadURL
  • Delete
  • Stat
  • Get
  • Put

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

  1. Register or sign in.
  2. Create a gallery with title, client, and description.
  3. Drag in photos or videos.
  4. Uploads go directly to MinIO using presigned URLs with per-file progress.
  5. Set a cover image, ordering, layout, appearance, branding, password, expiry, and client controls.
  6. Preview the exact public renderer.
  7. Publish and copy /g/:slug.

Client

  1. Open the private gallery link.
  2. Enter a gallery password if required.
  3. Browse responsive image and video media.
  4. Open photos in an immersive fullscreen viewer.
  5. Favorite photographs anonymously.
  6. 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 hashes
  • galleries: ownership, slug, publication state, controls, expiry, theme, and branding JSON
  • media: original object metadata, preview keys, processing state, dimensions, and ordering
  • favorites: anonymous gallery-scoped favorite records
  • downloads: individual download audit records
  • download_jobs: asynchronous ZIP status and object key

Migration files:

  • migrations/003_gallery_platform.sql for PostgreSQL
  • migrations/sqlite/003_gallery_platform.sql for 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
S
Description
No description provided
Readme
366 KiB
Languages
Go 41.2%
TypeScript 33.6%
CSS 24.3%
JavaScript 0.3%
Makefile 0.2%
Other 0.4%