# Start development server (runs both web and SSR)
pnpm dev
# Start only web development server
pnpm --filter web dev
# Start only SSR development server
pnpm --filter @afilmory/ssr dev
# Build production version
pnpm build
# Build manifest from storage (generates photo metadata)
pnpm run build:manifest
# Force rebuild all photos and metadata
pnpm run build:manifest -- --force
# Force regenerate thumbnails only
pnpm run build:manifest -- --force-thumbnails
# Force regenerate manifest only
pnpm run build:manifest -- --force-manifest# Generate database migrations
pnpm --filter @afilmory/ssr db:generate
# Run database migrations
pnpm --filter @afilmory/ssr db:migrate# Lint and fix code
pnpm lint
# Format code
pnpm format
# Type check (web app)
pnpm --filter web type-checkThe project employs a sophisticated, modular architecture that separates concerns across different applications and packages, enabling independent development, deployment, and scaling.
The project is divided into four main applications:
-
apps/web- Standalone Frontend SPA- Description: A pure client-side application built with React, Vite, and TypeScript. It can be deployed independently as a static website and is fully functional on its own.
- UI/Design: Dark-only, content-first gallery UI built on Apple's platform language — UIKit semantic colour tokens, translucent
bg-material-*surfaces over backdrop blur, and SwiftUI-style spring motion. Depth comes from stacked translucency rather than drop shadows, and the accent colour is derived from the photo being viewed.DESIGN.mdat the repo root is the normative spec; read it before non-trivial UI work. - Server Integration: It can operate in two modes:
- Standalone: Functions without a server, using a pre-built
photos-manifest.jsonfile. - Server-Connected: When a global variable like
window.__MANIFEST__is detected, it unlocks enhanced features. This injection is handled by eitherapps/ssr(from a static file) orbe/apps/core(from the database).
- Standalone: Functions without a server, using a pre-built
-
apps/ssr- Next.js Wrapper for SEO & Prerendering- Description: A Next.js application that acts as a transparent proxy for the
apps/webSPA. Its primary role is to enhance the frontend with server-side capabilities for performance and discoverability, rather than serving as a full-fledged backend. It injects the manifest from a static JSON file. - Key Features:
- OG (Open Graph) Rendering: Dynamically generates social media preview cards for shared links.
- SEO Metadata Injection: Injects dynamic SEO tags into the HTML for better search engine visibility.
- SSR for Shared Pages: Server-renders specific pages to provide fast initial load times.
- Description: A Next.js application that acts as a transparent proxy for the
be/apps/core: The complete backend server (Hono) for real-time data. For a detailed breakdown of its architecture, seebe/apps/core/AGENTS.md.be/apps/dashboard: The administration panel for the backend, using a linear, data-first admin aesthetic (crisp frames, subtle gradients). Seebe/apps/dashboard/AGENTS.mdfor full UI guidelines.
This is a pnpm workspace with multiple applications and packages:
apps/web/- Main frontend React application (Vite + React 19 SPA).apps/ssr/- Next.js 15 application serving as an SPA host and dynamic SEO/OG generator.be/apps/core/- The complete backend server (Hono) for real-time data.be/apps/dashboard/- The administration panel for the backend (linear, data-first admin look).packages/builder/- Photo processing and manifest generation tool.packages/webgl-viewer/- High-performance WebGL-based photo viewer component.packages/data/- Shared data access layer and PhotoLoader singleton.packages/components/- Reusable UI components shared across apps.packages/ui/- Core UI elements and design system components.packages/hooks/- Shared React hooks.packages/utils/- Utility functions.
Dual Server Architecture (for apps/ssr):
- Development Mode:
apps/ssr/src/app/[...all]/route.tscatches all SPA routes and servesindex.htmlwith injected manifest data from the static JSON file. - Production Mode: Next.js serves pre-built Vite SPA assets while providing dynamic OG image generation.
Dynamic SEO Implementation:
apps/ssr/src/index.html.ts- Pre-compiled HTML template with manifest data injected aswindow.__MANIFEST__.- Dynamic OG images generated per photo via Next.js API routes (
/og/[photoId]/route.ts). - HTML meta tags dynamically replaced for social media sharing.
Two-Layer Configuration System:
-
Builder Config (
builder.config.ts) - Infrastructure/Processing Layer:- Controls photo processing, storage connections, and build performance.
- Handles remote git repository sync for manifest/thumbnails.
- Configures multi-process/cluster processing for large photo sets.
-
Site Config (
site.config.ts+config.json) - Presentation/Content Layer:{ name: "Gallery Name", description: "...", author: { name: "...", url: "...", avatar: "..." }, social: { twitter: "...", github: "..." }, map: ["maplibre"] // Map provider configuration }
- Controls site branding, author info, social links.
- Merged with user
config.json. - Consumed by both SPA and SSR/Backend for consistent branding.
Builder Pipeline (packages/builder/src/cli.ts):
- Storage Sync: Downloads photos from S3/GitHub with incremental change detection.
- Format Processing: HEIC→JPEG, TIFF→web formats, Live Photo detection.
- Multi-threaded Processing: Configurable worker pools or cluster mode for performance.
- EXIF & Metadata Extraction: Camera settings, GPS, Fujifilm recipes, tone analysis.
- Thumbnail Generation: Multiple sizes with blurhash placeholders.
- Manifest Serialization: Generates
photos-manifest.jsonwith full metadata. - Remote Sync: Pushes updates to a git repository if configured.
SPA Data Consumption (packages/data/src/index.ts):
class PhotoLoader {
constructor() {
this.photos = window.__MANIFEST__.data // Injected via global
this.cameras = window.__MANIFEST__.cameras
this.lenses = window.__MANIFEST__.lenses
// Creates lookup maps and provides data access layer
}
}Data Flow Scenarios:
- Static/SSR Flow:
- Builder generates
photos-manifest.json. apps/ssrreads the JSON and injects it into the HTML aswindow.__MANIFEST__.- SPA's
PhotoLoaderconsumes the global data.
- Builder generates
- Full Backend Flow:
be/apps/corefetches data from the database.- It generates the manifest object in memory.
- It injects the manifest into the HTML as
window.__MANIFEST__before serving the page. - SPA's
PhotoLoaderconsumes the global data, unaware of the source.
- Frontend: React 19, TypeScript, Vite, Tailwind CSS, Jotai (state), TanStack Query
- SSR Layer: Next.js 15
- Backend: Hono, Drizzle ORM, PostgreSQL, tsyringe (for DI)
- Image Processing: Sharp, exiftool-vendored, HEIC conversion, blurhash generation
- Storage: S3-compatible (AWS/MinIO), GitHub repository storage
- Build System: pnpm workspaces, concurrent dev servers, cluster-based processing
- Concurrent Development:
pnpm devruns both SPA (Vite) and SSR (Next.js) servers. Usepnpm --filter @afilmory/core devto run the full backend. - Hot Reloading: SPA changes reflect immediately.
- Manifest Building:
pnpm run build:manifestprocesses photos and updates the staticphotos-manifest.json. - Type Safety: Shared types between builder, SPA, and servers ensure consistency.
- Page Structure: Keep files under
pages/as thin routing shells; move reusable UI/logic intomodules/<domain>/**. - State Isolation: When a UI feature has deep subtrees (e.g., photo library actions), do not thread handler props through multiple layers. Lift shared logic into colocated contexts or local stores (Jotai/Zustand) that expose hooks and let the consuming components trigger actions directly. This minimizes prop drilling and avoids unnecessary React re-renders.
- Avoid code duplication - extract common types and components.
- Keep components focused - use hooks and component composition.
- Follow React best practices - proper Context usage, state management.
- Use TypeScript strictly - leverage type safety throughout.
- Build React features out of small, atomic components. Push data fetching, stores, and providers down to the feature or tab that actually needs them so switching views unmounts unused logic and prevents runaway updates instead of centralizing everything in a mega component.
- Use flat keys with
.separation (e.g.,exif.camera.model). - Support pluralization with
_oneand_othersuffixes. - Modify English first, then other languages (ESLint auto-removes unused keys).
- CRITICAL: Avoid nested key conflicts in flat structure.
- During build, flat dot-separated keys are automatically converted to nested objects. A key cannot be both a string value AND a parent object.
- ❌ WRONG:
"action.tag.mode.and": "AND"+"action.tag.mode.and.tooltip": "..." - ✅ CORRECT:
"action.tag.mode.and": "AND"+"action.tag.tooltip.and": "..." - ❌ WRONG:
"photo.share.preview": "Share preview"+"photo.share.preview.download": "Download preview" - ✅ CORRECT:
"photo.share.preview": "Share preview"+"photo.share.downloadPreview": "Download preview" - Rule: If a key
a.b.cexists as a string value, you cannot usea.b.c.das a child key. Use a different parent path likea.b.dor rename the child key to avoid the conflict.
- Check
README.mdandpackage.jsonscripts for test commands. - Verify builds work with
pnpm build. - Test photo processing with
pnpm run build:manifest. - Validate types with
pnpm --filter web type-check.
This project contains multiple web applications with distinct design systems.
apps/web+packages/ui: The user-facing photo gallery.DESIGN.mdat the repo root is the normative spec — dark-only, UIKit semantic colour tokens,bg-material-*surfaces over role-matched backdrop blur, spring motion, and a fixed z-index tier table. It also lists the files that violate the system and must not be copied (§13).apps/web/AGENTS.mdis a short pointer to it. Theafilmory-web-designskill carries the same rules in lookup-table form.be/apps/dashboard: Guidelines for the functional, data-driven UI of the administration panel (linear, data-first aesthetic). Seebe/apps/dashboard/AGENTS.mdfor details.
Avoid feature gates/flags and any backwards compability changes - since our app is still unreleased" is really helpful.