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
Admin gallery (/gallery)
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:
processUploadedPhoto()inphotos.ts— resizes to max 1500px, saves JPEG + WebP- Inserts row into
gallery_itemsPostgres table - Calls
regenerateManifest()(updatesphotos-manifest.jsonfor Vite hash plugin) - 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):
- CSS-trap distortion — hue rotation applied to the file on disk; the distorted JPEG/WebP is what gets served at
/photos/<filename> - Restore key — a 6-hex-char key (
hue_hi hue_lo flags) stored incss-traps.json(alongside the photos) - In-browser WASM restoration —
WasmImage.tsxfetches the distorted file, loads/wasm/image-restore.wasm, runsrestore_pixels()to undo the hue rotation before display - 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: unprotected → processing → protected / failed.
Public gallery (/gallery on transquinnftw.com)
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.webp→ae2a190b2cc4.webp)getPhotoRestoreKey(src)— looks up the 6-char WASM key fromvirtual:photo-css-traps(baked into the JS bundle at build time fromcss-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) |