An anti-social media photo gallery, featuring NO friends, likes, follows, or shares. Built with SvelteKit and deployed on Cloudflare Workers.
My instance as an example: silklag.com
/admin/api/images/admin/api/images/{imageId}The application looks up the username from the domain via KV, loads user configuration, and fetches images from D1:
domain:example.com → "alice" → KV: user:alice → D1: SELECT * FROM images WHERE username = 'alice'git clone https://github.com/chronon/photochron.git
cd photochron
pnpm install
Copy the example files and customize them:
cp config/app-example.json config/app.jsonc
cp .dev.vars.example .dev.vars
Edit config/app.jsonc with your settings:
Edit .dev.vars with your local development secrets
Create and migrate your D1 database:
# Create D1 database
pnpm wrangler d1 create photochron
# Run migrations
pnpm wrangler d1 migrations apply photochron --local # For local dev
pnpm wrangler d1 migrations apply photochron # For production
pnpm dev # http://localhost:5173
pnpm run deploy
Configuration is stored in Cloudflare KV with these keys:
global: Global config (image CDN settings)domain:HOSTNAME: Domain-to-username mapping (e.g., domain:example.com → "alice")user:USERNAME: Per-user config (domains array, avatar, authorized client IDs)These are automatically generated from config/app.jsonc during deployment via build scripts.
Image metadata is stored in Cloudflare D1. See migrations/ for the complete schema including tables and indexes.
All admin endpoints use Cloudflare Access authentication with service tokens:
Headers:
CF-Access-Client-Id: your-service-token-client-id
CF-Access-Client-Secret: your-service-token-client-secret
Upload - POST /admin/api/images
Upload a photo with metadata. Requires Content-Type: multipart/form-data.
Request body: file (image file), metadata (JSON with name, caption, captured date)
Response: { success: true, id, filename, uploaded }
Lookup - GET /admin/api/images/by-name/{photoName}
Find image ID by photo name (case-insensitive). Returns most recent if multiple matches exist.
Response: { success: true, id, name, captured, uploaded }
Delete - DELETE /admin/api/images/{imageId}
Delete a photo. Verifies ownership before deletion.
Response: { success: true, id, message }
There is no admin interface, just API endpoints for add, find, and delete. For Apple devices these two shortcuts make adding and deleting photos from share sheets fast and seamless.
All /admin/* routes use two-layer security: Cloudflare Access validates credentials at the edge, then SvelteKit hooks verify JWT claims and check client IDs against authorized lists in KV.
Copy .dev.vars.example to .dev.vars and fill in your values. When CF_ACCESS_TEAM_DOMAIN=dev, authentication is bypassed. This only activates locally (.dev.vars is not deployed).
# Development
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
# Quality Assurance
pnpm check # Type check with svelte-check
pnpm check:all # Run all checks (check + lint + test)
pnpm lint # Check formatting and linting
pnpm format # Format code with Prettier
# Testing
pnpm test # Run unit tests with Vitest
# Configuration & Deployment
pnpm config:build # Generate wrangler.jsonc and KV data
pnpm config:deploy # Build config and upload to KV
pnpm run deploy # Full deployment (config + build + deploy)
pnpm run deploy:preview # Preview deployment (dry run)
The application uses Cloudflare KV (configuration), D1 (image metadata), Images (photo storage), and Access (authentication).
Request Flow:
domain:*) → Fetch user config from KV (user:*) → Query D1 for images/admin/* path if not already doneconfig/app.jsonc: Add user entry with domains array (one or more domains), avatar, and authorized client IDs (include service token client ID)pnpm run deploy to generate config (including domain mappings), upload to KV, and deployfavicon16 (16x16), favicon32 (32x32), apple180 (180x180) variants for the user's avatar in Cloudflare Images. Falls back to static files if not present.config/app.jsonc: Update user propertiespnpm run deploy to regenerate and sync changes to KVMIT License - see LICENSE file for details.