Languages: English | 中文
A smooth photo display and management application, supporting multiple image formats and large-size image rendering.
- 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
- 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
- D1 stores application and photo metadata through the
DBbinding. - 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.
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-versionselects 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_SECRETThe 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.
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.
- Click avatar to sign in with GitHub OAuth or use email/password login
- Go to the dashboard at /dashboard
- On the Photos page, select and upload images (supports batch & drag-and-drop)
- The Worker stores images in Hosted Images, sends videos directly from the browser to Stream, and reserves R2 for other object types
- Node.js 22.12+
- pnpm 10+
- A Cloudflare account with D1, R2, Workers, paid Images storage, and Stream enabled
# With pnpm (recommended)
pnpm install
# Or with other package managers
npm install
yarn install# Generate Worker binding types and initialize local D1.
pnpm cf:typegen
pnpm d1:migrate:localpnpm dev:workerApp 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
# 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 deployContributions are welcome! Please:
- Fork the repo
- Create a feature branch (git checkout -b feature/amazing-feature)
- Commit changes (git commit -m 'Add some amazing feature')
- Push to branch (git push origin feature/amazing-feature)
- 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
- Email: master@uniiem.com
- GitHub: @HoshinoSuzumi
- Website: bh8.ga
- Gallery: lens.bh8.ga
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.
This project was inspired by Afilmory, another excellent personal gallery project.
Thanks to the following open-source projects and libraries:



