323 lines
9.1 KiB
Markdown
323 lines
9.1 KiB
Markdown
# Northline Delivery Studio
|
|
|
|
Northline is an original photographer client-delivery platform inspired by the category of premium gallery products such as DLVRD. It is built around one loop:
|
|
|
|
```text
|
|
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+, `net/http`, 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:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
npm --prefix frontend install
|
|
docker compose up -d --wait
|
|
make migrate
|
|
make seed
|
|
```
|
|
|
|
Start the API:
|
|
|
|
```bash
|
|
make dev
|
|
```
|
|
|
|
Start the frontend in a second terminal:
|
|
|
|
```bash
|
|
make frontend-dev
|
|
```
|
|
|
|
Open:
|
|
|
|
- Dashboard: [http://localhost:5173/login](http://localhost:5173/login)
|
|
- Dev diagnostics: [http://localhost:5173/dashboard/dev](http://localhost:5173/dashboard/dev)
|
|
- Demo client gallery: [http://localhost:5173/g/emma-james-wedding](http://localhost:5173/g/emma-james-wedding)
|
|
- MinIO console: [http://localhost:9001](http://localhost:9001)
|
|
|
|
### PowerShell
|
|
|
|
```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:
|
|
|
|
```powershell
|
|
go -C backend run ./cmd/server
|
|
npm --prefix frontend run dev
|
|
```
|
|
|
|
## Demo Credentials
|
|
|
|
The seed command creates:
|
|
|
|
```text
|
|
Email: demo@example.com
|
|
Password: DemoPassword123!
|
|
Studio: Northline Studio
|
|
```
|
|
|
|
It also creates the published demo gallery:
|
|
|
|
```text
|
|
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:
|
|
|
|
```dotenv
|
|
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:
|
|
|
|
```text
|
|
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.
|
|
|
|
```dotenv
|
|
DB_DRIVER=sqlite
|
|
SQLITE_PATH=./data/surprise.db
|
|
```
|
|
|
|
Then run the same migration and seed commands:
|
|
|
|
```bash
|
|
make migrate
|
|
make seed
|
|
make dev
|
|
```
|
|
|
|
PowerShell process variables use `$env:` syntax:
|
|
|
|
```powershell
|
|
$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:
|
|
|
|
```text
|
|
POST /api/auth/register
|
|
POST /api/auth/login
|
|
POST /api/auth/logout
|
|
GET /api/auth/me
|
|
```
|
|
|
|
Authenticated galleries:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
GET /health
|
|
```
|
|
|
|
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
|
|
|
|
```text
|
|
backend/
|
|
cmd/server/ API 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:
|
|
|
|
```bash
|
|
```
|
|
|
|
Frontend:
|
|
|
|
```bash
|
|
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
|