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
- Multi-user support - One deployment serves unlimited domains/users
- Domain-based routing - Explicit domain-to-username mappings in KV (supports multiple domains per user and arbitrary mappings)
- KV-based configuration - All config (global settings, domain mappings, user config) stored in Cloudflare KV
- D1 database - Image metadata stored in Cloudflare D1 for fast querying
- Photo upload - Upload photos via authenticated API endpoint at
/admin/api/images - Photo deletion - Delete photos via authenticated API endpoint at
/admin/api/images/{imageId} - Infinite scroll - Smooth loading of photo galleries
- Dynamic favicons - User-specific favicons and touch icons per domain
- Cloudflare Images integration - Optimized image delivery and storage
- Cloudflare Access - Service token authentication for secure uploads
The application looks up the username from the domain via KV, loads user configuration, and fetches images from D1:
- Domain → KV Lookup → Username → User Config → D1 Images
- example.com → KV:
domain:example.com→"alice"→ KV:user:alice→ D1:SELECT * FROM images WHERE username = 'alice' - example.com/admin/api/images → Domain lookup → Authenticated upload → Cloudflare Images + D1 insert
git clone https://github.com/chronon/photochron.git
cd photochron
pnpm installCopy the example files and customize them:
cp config/app-example.json config/app.jsonc
cp .dev.vars.example .dev.varsEdit config/app.jsonc with your settings:
- Update Cloudflare resource IDs (KV namespace, D1 database, Images delivery URL)
- Add your users with their domains array (one or more domains per user), avatars, and authorized client IDs
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 productionpnpm dev # http://localhost:5173pnpm run deployConfiguration 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.
- Add: https://www.icloud.com/shortcuts/d44f9cce647f4acd837f346fddeefe2f
- Delete: https://www.icloud.com/shortcuts/ad0dc21dbab84deebbba42fe72507453
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:
- Look up username from domain in KV (
domain:*) → Fetch user config from KV (user:*) → Query D1 for images - Admin routes determine username from domain → Validate via Cloudflare Access at edge → Check authorization in KV → Process operation
- Set up Cloudflare Access (one-time): Configure Access application for
/admin/*path if not already done - Create Service Token: Generate Cloudflare Access service token for upload authentication
- Edit
config/app.jsonc: Add user entry withdomainsarray (one or more domains), avatar, and authorized client IDs (include service token client ID) - Deploy: Run
pnpm run deployto generate config (including domain mappings), upload to KV, and deploy - Create favicon variants (optional): Add
favicon16(16x16),favicon32(32x32),apple180(180x180) variants for the user's avatar in Cloudflare Images. Falls back to static files if not present. - Configure upload client: Provide user with service token credentials and upload endpoint URL
- Edit
config/app.jsonc: Update user properties - Deploy: Run
pnpm run deployto regenerate and sync changes to KV - Changes take effect immediately after deployment
MIT License - see LICENSE file for details.