Skip to content
 
 

Latest commit

 

History

615 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ChronoFrame

Chronoframe

Latest Release Latest Nightly Release License

Discord Server

Featured|HelloGitHub ChronoFrame - Self-hosted photo gallery for photographers. | Product Hunt

Languages: English | 中文

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

Live Demo: TimoYin's Mems

✨ Features

🖼️ Powerful Photo Management

  • Manage photos online - Easily manage and browse photos via the web interface
  • Explore map - Browse photo locations on a map
  • Photo metadata - Preserves supported source metadata for capture time, location, and camera details
  • Reverse geocoding - Automatically identifies photo shooting locations
  • Multi-format support - Supports mainstream formats including JPEG, PNG, HEIC/HEIF
  • Cloudflare delivery - Original bytes and dynamically transformed WebP thumbnails at the edge

🔧 Modern Tech Stack

  • 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

☁️ Cloudflare-native Storage

  • D1 stores application and photo metadata through the DB binding.
  • Cloudflare Images Hosted Images stores every image through IMAGES.
  • Cloudflare Stream stores and delivers every video through STREAM.
  • R2 stores other non-image, non-video objects through MEDIA_BUCKET.
  • Workers Assets serves the built Nuxt client through ASSETS.

☁️ Deploy to Cloudflare Workers

Cloud deployments use Cloudflare Workers Builds with GitHub integration. Connect Pitrick3141/chronoframe from the existing chronoframe Worker's Settings > Builds, with production branch main and root directory /:

  • Build command: pnpm run build:cloudflare
  • Deploy command: pnpm run deploy:cloudflare
  • Build variable: PNPM_VERSION=10.34.1; .node-version selects Node 22.

Disable non-production branch builds and grant the Cloudflare deployment token D1 Edit. Runtime secrets remain in the Worker's Variables & Secrets. See the GitHub integration setup.

ChronoFrame is Workers-only. The current bundle requires the Workers Paid plan because it exceeds the Workers Free 3 MB compressed script limit. A Cloudflare account with paid Cloudflare Images storage and Cloudflare Stream enabled is also required. Hosted Images accepts files up to 10 MiB; supported inputs are JPEG, PNG, GIF, WebP, SVG, and HEIC. AVIF input requires Enterprise. The public Worker route returns a metadata-stripped WebP display image capped at 4096 px and generates 600 px WebP thumbnails through the Images binding; the raw Hosted Image source is administrator-only. Account-level delivery variants are not used. Stream is billed by minutes stored and minutes delivered.

For videos, the Worker uses the STREAM binding to create a one-time Direct Creator Upload URL, the browser sends a multipart POST directly to Stream, and playback uses HLS after processing. Cloudflare's binding supports this basic POST flow for files under 200 MB; ChronoFrame therefore defaults to 199999999 bytes. The default maximum duration is 600 seconds. No Stream API token is exposed to the app or browser.

For one-off resource setup or a manual local deployment:

pnpm install
pnpm exec wrangler login

# Create D1 and copy the returned database_id into wrangler.jsonc.
pnpm d1:create

# Create the R2 bucket named by wrangler.jsonc.
pnpm exec wrangler r2 bucket create chronoframe-storage

# Enable Cloudflare Stream in the dashboard; STREAM uses a binding, not an app token.

# Store two independent random values for the first deployment.
pnpm exec wrangler secret put NUXT_SESSION_PASSWORD
pnpm exec wrangler secret put CFRAME_BOOTSTRAP_TOKEN

# Build, apply pending D1 migrations, and deploy.
pnpm run deploy

# Register the Stream webhook with the deployed URL, then store its result.secret.
pnpm exec wrangler secret put CFRAME_STREAM_WEBHOOK_SECRET

The bindings in wrangler.jsonc must remain DB, IMAGES, STREAM, MEDIA_BUCKET, and ASSETS. See the Workers deployment guide for resource setup, local development, CI, limits, and custom domains.

The current branch no longer contains the legacy Docker build, Compose stack, or image-publishing workflow because they cannot provide the required Worker bindings. For migration reference, inspect a release or Git tag from the pre-Workers line.

📖 User Guide

On first launch, open the onboarding wizard and create the administrator account. The wizard requires the CFRAME_BOOTSTRAP_TOKEN; ChronoFrame has no default administrator password.

Logging into the Dashboard

  1. Click avatar to sign in with GitHub OAuth or use email/password login

Uploading Photos

  1. Go to the dashboard at /dashboard
  2. On the Photos page, select and upload images (supports batch & drag-and-drop)
  3. The Worker stores images in Hosted Images, sends videos directly from the browser to Stream, and reserves R2 for other object types

📸 Screenshots

Gallery Photo Detail Map Explore Dashboard

🛠️ Development

Requirements

  • Node.js 22.12+
  • pnpm 10+
  • A Cloudflare account with D1, R2, Workers, paid Images storage, and Stream enabled

Install dependencies

# With pnpm (recommended)
pnpm install

# Or with other package managers
npm install
yarn install

Initialize database

# Generate Worker binding types and initialize local D1.
pnpm cf:typegen
pnpm d1:migrate:local

Start development server

pnpm dev:worker

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

Project Structure

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

Build commands

# Local Worker runtime; Stream E2E checks require an account-backed preview
pnpm dev:worker

# Build only dependencies
pnpm build:deps

# Production Worker build
pnpm build:worker

# Database operations
pnpm d1:generate          # Generate migration files
pnpm d1:migrate:local     # Apply migrations locally
pnpm d1:migrate:remote    # Apply migrations to production

# Deploy to Cloudflare Workers
pnpm run deploy

🤝 Contributing

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

Coding Guidelines

  • Use TypeScript for type safety
  • Follow ESLint and Prettier conventions
  • Update documentation accordingly

📄 License

This project is licensed under the MIT License.

👤 Author

Timothy Yin

  • Email: master@uniiem.com
  • GitHub: @HoshinoSuzumi
  • Website: bh8.ga
  • Gallery: lens.bh8.ga

❓ FAQ

How is the admin user created?

Open the first-run onboarding wizard, authenticate it with the CFRAME_BOOTSTRAP_TOKEN, and choose the administrator email, display name, and password. ChronoFrame never ships a default administrator credential.

Which image formats are supported?

Hosted Images accepts JPEG, PNG, GIF, WebP, SVG, and HEIC, up to 10 MiB per image. AVIF input requires Cloudflare Enterprise. Every supported video upload, including Live/Motion Photo companions, is stored and delivered by Cloudflare Stream.

Can I use S3, local, OpenList, or GitHub storage?

The Workers version has fixed bindings: images use Hosted Images, videos use Stream, other objects use R2, and records use D1. The earlier storage providers are available only in legacy container releases and Git tags.

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. Configure MapLibre or Mapbox with the corresponding NUXT_PUBLIC_* variables in the configuration guide.

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?

There is no automatic scanner/importer. Follow the migration checklist to transform SQLite records and move Images, Stream, and R2 objects explicitly.

🙏 Acknowledgements

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

Thanks to the following open-source projects and libraries:

⭐️ Star History

Star History Chart

About

Self-hosted personal gallery application with online photo management and albums, supporting Live/Motion Photos, EXIF parsing, geolocation recognition, and an explore map.

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages