Show HN: ChronoFrame – A self-hosted personal photo gallery app

2 hours ago 2

Chronoframe Logo
License Nuxt TypeScript WebGL

Languages: English | 中文

A smooth photo display and management application, supporting multiple image formats and large-size image rendering.

Live Demo: TimoYin's Mems

🖼️ Powerful Photo Management

  • Manage photos online - Easily manage and browse photos via the web interface
  • Explore map - Browse photo locations on a map
  • Smart EXIF parsing - Automatically extracts metadata such as capture time, geolocation, and camera parameters
  • Reverse geocoding - Automatically identifies photo shooting locations
  • Multi-format support - Supports mainstream formats including JPEG, PNG, HEIC/HEIF
  • Smart thumbnails - Efficient thumbnail generation using ThumbHash
  • Nuxt 4 - Built on the latest Nuxt framework with SSR/SSG support
  • TypeScript - Full type safety
  • TailwindCSS - Modern CSS framework
  • Drizzle ORM - Type-safe database ORM

☁️ Flexible Storage Solutions

  • Multiple storage backends - Supports S3-compatible storage, GitHub (WIP), and local filesystem (WIP)
  • CDN acceleration - Configurable CDN URL for faster photo delivery

We recommend deploying with the prebuilt Docker image. View the image on ghcr

Run with customized environment variables:

docker run -d \ --name chronoframe \ -p 3000:3000 \ -v $(pwd)/data:/app/data \ -e CFRAME_ADMIN_EMAIL="" \ -e CFRAME_ADMIN_NAME="" \ -e CFRAME_ADMIN_PASSWORD="" \ -e NUXT_PUBLIC_APP_TITLE="" \ -e NUXT_PUBLIC_APP_SLOGAN="" \ -e NUXT_PUBLIC_APP_AUTHOR="" \ -e NUXT_PUBLIC_APP_AVATAR_URL="" \ -e NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN="" \ -e NUXT_MAPBOX_ACCESS_TOKEN="" \ -e NUXT_STORAGE_PROVIDER="s3" \ -e NUXT_PROVIDER_S3_ENDPOINT="" \ -e NUXT_PROVIDER_S3_BUCKET="chronoframe" \ -e NUXT_PROVIDER_S3_REGION="auto" \ -e NUXT_PROVIDER_S3_ACCESS_KEY_ID="" \ -e NUXT_PROVIDER_S3_SECRET_ACCESS_KEY="" \ -e NUXT_PROVIDER_S3_PREFIX="photos/" \ -e NUXT_PROVIDER_S3_CDN_URL="" \ -e NUXT_OAUTH_GITHUB_CLIENT_ID="" \ -e NUXT_OAUTH_GITHUB_CLIENT_SECRET="" \ -e NUXT_SESSION_PASSWORD="" \ ghcr.io/hoshinosuzumi/chronoframe:latest

Create a .env file:

# Admin user email (required) CFRAME_ADMIN_EMAIL= # Admin user name (default to Chronoframe, optional) CFRAME_ADMIN_NAME= # Admin user password (default to CF1234@!, optional) CFRAME_ADMIN_PASSWORD= # App info NUXT_PUBLIC_APP_TITLE= NUXT_PUBLIC_APP_SLOGAN= NUXT_PUBLIC_APP_AUTHOR= NUXT_PUBLIC_APP_AVATAR_URL= # Mapbox access token for map features, Mapbox GL JS (Client-side, public) NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN= # Mapbox secret access token for server-side, Mapbox Search API (Reverse Geocoding) NUXT_MAPBOX_ACCESS_TOKEN= # Storage provider (s3/github/local) NUXT_STORAGE_PROVIDER=s3 # S3 storage config NUXT_PROVIDER_S3_ENDPOINT= NUXT_PROVIDER_S3_BUCKET=chronoframe NUXT_PROVIDER_S3_REGION=auto NUXT_PROVIDER_S3_ACCESS_KEY_ID= NUXT_PROVIDER_S3_SECRET_ACCESS_KEY= NUXT_PROVIDER_S3_PREFIX=photos/ NUXT_PROVIDER_S3_CDN_URL= # Session password (32 chars, required) NUXT_SESSION_PASSWORD= # GitHub OAuth NUXT_OAUTH_GITHUB_CLIENT_ID= NUXT_OAUTH_GITHUB_CLIENT_SECRET=

Create docker-compose.yml:

services: chronoframe: image: ghcr.io/hoshinosuzumi/chronoframe:latest container_name: chronoframe restart: unless-stopped ports: - '3000:3000' volumes: - ./data:/app/data env_file: - .env

Start:

Variable Description Default Required
CFRAME_ADMIN_EMAIL Email of the initial admin user None Yes, must be the GitHub account email used for login
CFRAME_ADMIN_NAME Username of the initial admin Chronoframe No
CFRAME_ADMIN_PASSWORD Password of the initial admin CF1234@! No
NUXT_PUBLIC_APP_TITLE Application title ChronoFrame No
NUXT_PUBLIC_APP_SLOGAN Application slogan None No
NUXT_PUBLIC_APP_AUTHOR Application author None No
NUXT_PUBLIC_APP_AVATAR_URL Application avatar URL None No
NUXT_PUBLIC_MAPBOX_ACCESS_TOKEN Mapbox access token for map service None Yes
NUXT_MAPBOX_ACCESS_TOKEN Mapbox access token for location info None No
NUXT_STORAGE_PROVIDER Storage provider (s3, github, local) s3 Yes
NUXT_PROVIDER_S3_ENDPOINT S3 endpoint None Required if provider is s3
NUXT_PROVIDER_S3_BUCKET S3 bucket name chronoframe Required if provider is s3
NUXT_PROVIDER_S3_REGION S3 bucket region auto Required if provider is s3
NUXT_PROVIDER_S3_ACCESS_KEY_ID S3 access key ID None Required if provider is s3
NUXT_PROVIDER_S3_SECRET_ACCESS_KEY S3 secret access key None Required if provider is s3
NUXT_PROVIDER_S3_PREFIX S3 object prefix photos/ No
NUXT_PROVIDER_S3_CDN_URL S3 CDN URL None No
NUXT_OAUTH_GITHUB_CLIENT_ID GitHub OAuth app Client ID None Yes
NUXT_OAUTH_GITHUB_CLIENT_SECRET GitHub OAuth app Client Secret None Yes
NUXT_SESSION_PASSWORD Session encryption password (32 chars) None Yes

If CFRAME_ADMIN_EMAIL and CFRAME_ADMIN_PASSWORD are not set, the default admin account is:

Logging into the Dashboard

  1. Click avatar to sign in with GitHub OAuth or use email/password login
  1. Go to the dashboard at /dashboard
  2. On the Photos page, select and upload images (supports batch & drag-and-drop)
  3. System will automatically parse EXIF data, generate thumbnails, and perform reverse geocoding

Gallery Photo Detail Map Explore Dashboard

  • Node.js 18+
  • pnpm 9.0+
# With pnpm (recommended) pnpm install # Or with other package managers npm install yarn install

Configure environment variables

# 2. Generate migration files (optional) pnpm db:generate # 3. Run database migrations pnpm db:migrate

App will start at http://localhost:3000.

chronoframe/ ├── app/ # Nuxt app │ ├── components/ # Components │ ├── pages/ # Page routes │ ├── composables/ # Composables │ └── stores/ # Pinia stores ├── packages/ │ └── webgl-image/ # WebGL image viewer ├── server/ │ ├── api/ # API routes │ ├── database/ # DB schema & migrations │ └── services/ # Business logic services └── shared/ # Shared types & utils
# Development (with dependencies build) pnpm dev # Build only dependencies pnpm build:deps # Production build pnpm build # Database operations pnpm db:generate # Generate migration files pnpm db:migrate # Run migrations # Preview production build pnpm preview

Contributions are welcome! Please:

  1. Fork the repo
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit changes (git commit -m 'Add some amazing feature')
  4. Push to branch (git push origin feature/amazing-feature)
  5. Open a Pull Request
  • Use TypeScript for type safety
  • Follow ESLint and Prettier conventions
  • Update documentation accordingly

This project is licensed under the MIT License.

Timothy Yin

How is the admin user created?

On first startup, an admin user is created based on CFRAME_ADMIN_EMAIL, CFRAME_ADMIN_NAME, and CFRAME_ADMIN_PASSWORD. The email must match your GitHub account email used for login.

Which image formats are supported?

Supported formats: JPEG, PNG, HEIC/HEIF, MOV (for Live Photos).

Why can’t I use GitHub/Local storage?

Currently only S3-compatible storage is supported. GitHub and local storage support is planned.

Why is a map service required and how to configure it?

The map is used to browse photo locations and render mini-maps in photo details. Currently Mapbox is used. After registering, get an access token and set it to the MAPBOX_TOKEN variable.

Why wasn’t my MOV file recognized as a Live Photo?

Ensure the image (.heic) and video (.mov) share the same filename (e.g., IMG_1234.heic and IMG_1234.mov). Upload order does not matter. If not recognized, you can trigger pairing manually from the dashboard.

How do I import existing photos from storage?

Direct import of existing photos is not yet supported. A directory scanning import feature is planned.

How is this different from Afilmory?

Afilmory generates a manifest from photos during local/CI processing and serves them statically. ChronoFrame is a dynamic photo management app, offering online upload, management, and browsing—better for frequently updated galleries. In other words, Afilmory = static; ChronoFrame = dynamic, online upload/manage.

This project was inspired by Afilmory, another excellent personal gallery project.

Thanks to the following open-source projects and libraries:

Stargazers over time

Read Entire Article