lilith-platform.live/docs/gallery-architecture.md
2026-04-20 03:30:58 -07:00

5.7 KiB

Gallery Architecture

How photos go from admin upload to public display on transquinnftw.com.


Overview

The gallery has three distinct layers:

Layer File Role
Admin codebase/@features/admin/ CRUD, upload, protection, reorder
Data API codebase/@features/provider-website/data-api/ Read-only snapshot server for the public site
Public frontend codebase/@features/provider-website/frontend-public/ Category filter, masonry grid, lightbox

Storage: two databases, one sync

Admin backend stores gallery items in Postgres (QUINN_ADMIN_DB_URL).
Data API reads a SQLite snapshot at deployments/@domains/quinn.admin/data/quinn.db.

After every admin mutation (upload, delete, update metadata, reorder), syncGalleryToSqlite() writes the full current gallery from Postgres into the SQLite file. The data-api picks up changes on its next request — no restart required.

Admin upload
    │
    ▼
Postgres gallery_items        ← source of truth for admin UI
    │
    ├── syncGalleryToSqlite() runs fire-and-forget
    │
    ▼
quinn.admin/data/quinn.db     ← SQLite snapshot read by data-api
    │
    ▼
data-api GET /api/data        ← served to the public site
    │
    ▼
useProviderData() hook        ← fetches on mount + 30s polling
    │
    ▼
GalleryPage / GalleryGrid     ← public display

Frontend: codebase/@features/admin/frontend-public/src/pages/GalleryPage.tsx
Backend routes: codebase/@features/admin/backend-api/src/routes/gallery.ts

Endpoints:

Method Path Action
GET /api/gallery List all items with restore keys + adversary metadata
POST /api/gallery Upload photo (multipart)
PUT /api/gallery/:id Update alt/category/featured
DELETE /api/gallery/:id Delete photo files + DB row
PUT /api/gallery/reorder Bulk sort_order update
POST /api/gallery/:id/protect Trigger adversarial image protection
POST /api/gallery/:id/adversary-view Trigger forensic view job (dev)

Upload flow:

  1. processUploadedPhoto() in photos.ts — resizes to max 1500px, saves JPEG + WebP
  2. Inserts row into gallery_items Postgres table
  3. Calls regenerateManifest() (updates photos-manifest.json for Vite hash plugin)
  4. Calls syncGalleryToSqlite() — writes snapshot to SQLite

Photo files are stored in PHOTOS_DIR (env var, required). In production this is the quinn.www public/photos/ directory so nginx serves them directly at /photos/<filename>.


Image protection

Each photo can be processed through the adversarial protection pipeline (image-protection service, port 3030):

  1. CSS-trap distortion — hue rotation applied to the file on disk; the distorted JPEG/WebP is what gets served at /photos/<filename>
  2. Restore key — a 6-hex-char key (hue_hi hue_lo flags) stored in css-traps.json (alongside the photos)
  3. In-browser WASM restorationWasmImage.tsx fetches the distorted file, loads /wasm/image-restore.wasm, runs restore_pixels() to undo the hue rotation before display
  4. Adversarial face cloaking — optional PGD attack perturbs face-detector features so scrapers cannot identify subjects

Scrapers downloading the raw files get colour-swapped images. Real users see the correct photo.

Protection status per item: unprotectedprocessingprotected / failed.


Component: GalleryGrid.tsx + Lightbox.tsx
Data hook: useProviderData() in provider-website/frontend-public/

Gallery items arrive in ProviderData.gallery[] from the data-api. Config (deployments/@domains/quinn.www/root/src/config.ts) supplies two functions:

  • rewritePhotoSrc(src) — maps original filenames to content-hashed Vite build filenames (e.g. cage-harness-purple.webpae2a190b2cc4.webp)
  • getPhotoRestoreKey(src) — looks up the 6-char WASM key from virtual:photo-css-traps (baked into the JS bundle at build time from css-traps.json)

Categories: glamour · casual · suggestive · headshot · lifestyle · portrait

Audience-aware default: clients land with headshot filter active; fans see all photos.


Bootstrap / first-run seeding

If the admin Postgres gallery_items table is empty on first startup, the migration 2026-04-20_seed_gallery_from_sqlite reads from the SQLite snapshot (if it exists and is non-empty) and imports existing rows. This handles promotion from an older SQLite-only deployment.


Key files

Path Purpose
codebase/@features/admin/backend-api/src/routes/gallery.ts Admin CRUD + protection routes
codebase/@features/admin/backend-api/src/photos.ts Upload processing (resize, WebP, manifest)
codebase/@features/admin/backend-api/src/sqlite-sync.ts Postgres → SQLite sync after mutations
codebase/@features/provider-website/data-api/src/server.ts Data API (reads SQLite, serves ProviderData JSON)
codebase/@features/provider-website/data-api/src/serialize.ts SQLite → ProviderData shape
codebase/@features/provider-website/frontend-public/src/pages/GalleryPage.tsx Public gallery page
codebase/@features/provider-website/frontend-public/src/components/Gallery/GalleryGrid.tsx Masonry grid + category filter
codebase/@features/provider-website/frontend-public/src/components/shared/WasmImage.tsx In-browser WASM pixel restoration
codebase/@features/admin/frontend-public/src/pages/GalleryPage.tsx Admin gallery UI (lens modes, adversary view)
deployments/@domains/quinn.admin/data/quinn.db SQLite snapshot (written by admin, read by data-api)