Architecture
Pixelflare is a self-hosted image CDN built on Cloudflare Workers, designed to be fast, scalable, and cost-effective. This document covers the overall architecture and design decisions.
Overview
The system is split into three main parts:
- Frontend - SvelteKit application for the web UI
- API - Hono-based REST API running on Cloudflare Workers
- CDN - Image serving endpoints with on-the-fly transformations
All three run on Cloudflare's edge network, putting compute and data close to users worldwide.
Monorepo Structure
The project uses a pnpm workspace monorepo with these packages:
/packages
├── api/ # Cloudflare Workers API (Hono)
├── frontend/ # SvelteKit web application
├── database/ # Drizzle ORM schemas and migrations
├── config/ # Shared configuration constants
├── shared/ # Shared TypeScript types
├── i18n/ # Internationalization files
├── gateway/ # Cloudflare Gateway configuration
├── docs/ # VitePress documentation
└── tooling/ # ESLint and Prettier configsEach package has its own package.json and can be built/tested independently. See Local Setup for development environment details.
Configuration Management
The config package centralizes all constants and configuration values (rate limits, file size limits, allowed image types, etc.). This ensures consistency across frontend and API, and makes it easy to adjust limits without hunting through the codebase. See Configuration Constants for the full list.
Technology Stack
Frontend
- SvelteKit 5 - Full-stack framework with SSR
- Tailwind CSS 4 - Utility-first styling
- DaisyUI - Component library
- Uppy - File upload handling
- Deployed to: Cloudflare Pages
API
- Hono - Lightweight web framework
- Drizzle ORM - Type-safe database queries
- Zod - Schema validation
- OpenAPI - API documentation
- Deployed to: Cloudflare Workers
Data Layer
- D1 - SQLite-compatible database
- R2 - S3-compatible object storage
- KV - Key-value cache
- Analytics Engine - Time-series analytics
- Queues - Message processing
- Vectorize - Vector database for semantic search
All data stays within Cloudflare's network for low latency and reduced costs.
Type System and Validation
One of the key architectural advantages of the monorepo is the shared type system between frontend and API.
How Types Flow
graph LR
A[Zod Schemas] --> B[Runtime Validation]
A --> C[TypeScript Types]
A --> D[OpenAPI Spec]
C --> E[Frontend Types]
C --> F[API Types]
D --> G[API Documentation]Zod Schemas
All validation schemas are defined in /packages/api/src/schemas/ using Zod. A single schema definition provides:
- Runtime validation - Validates incoming requests
- TypeScript types - Inferred via
z.infer<typeof schema> - OpenAPI docs - Generated automatically via
@hono/zod-openapi
Example:
// Define once
const createImageSchema = z.object({
filename: z.string().transform(validateFilename),
content_type: z.string().optional(),
// ...
});
// Get TypeScript type
type CreateImageRequest = z.infer<typeof createImageSchema>;
// Validate at runtime
const body = createImageSchema.parse(req.body);
// Generate OpenAPI docs automaticallyShared Types
The shared package contains types used by both frontend and API:
- Environment variable types
- Common interfaces
- Utility types
When the API schema changes, TypeScript catches any frontend code that needs updating at compile time, not runtime.
Validation Strategy
Input validation happens in layers:
- Schema validation - Zod schemas validate structure and types
- Transformation - Schemas can transform input (e.g., sanitize filenames)
- Business validation - Service layer checks ownership, quotas, etc.
- Database validation - Drizzle schema enforces constraints
See Validation for detailed patterns and Security for security considerations.
Frontend Architecture
The SvelteKit app is a server-side rendered application deployed to Cloudflare Pages. It handles:
- User interface and interactions
- Client-side routing with SvelteKit's file-based routing
- Server-side rendering for better SEO and initial load times
- API requests to the Workers backend
- Image uploads via Uppy with resumable uploads
Key patterns:
- Stores - Svelte 5 runes for reactive state (
$state,$derived) - Form actions - SvelteKit form handling
- Load functions - Server-side data fetching
- Layouts - Nested layouts for consistent UI
The frontend is stateless - all data lives in the API/database.
Internationalization
The app supports 18+ languages with the i18n package managing translations. Translation files are JSON with automated validation to catch missing keys or inconsistencies. The frontend detects user locale and loads translations dynamically. See Translations for adding new languages or updating existing ones.
API Architecture
The API runs as a Cloudflare Worker using the Hono framework. Request flow:
graph LR
A[Request] --> B[Middleware Stack]
B --> C{Route Type}
C -->|/v1/*| D[API Routes]
C -->|/cdn/*| E[CDN Routes]
C -->|/auth/*| F[Auth Routes]
D --> G[Service Layer]
E --> G
F --> G
G --> H[Data Layer]
H --> I[Response]Middleware Stack
Applied to all requests in order:
- Logger - Request/response logging
- CORS - Cross-origin policies (see Security)
- Security Headers - CSP, HSTS, X-Frame-Options
- Error Handler - Consistent error formatting
- Authentication - Multi-mode auth (see Authentication)
- Rate Limiting - Token bucket algorithm (see Rate Limiting)
- Audit Logging - Action tracking (see Audit Log)
Route Organisation
Routes are grouped by purpose:
/v1/*- Authenticated REST API for CRUD operations/cdn/*- Public CDN serving (images, profiles)/auth/*- Authentication endpoints (OAuth, sessions)/v1/docs- OpenAPI documentation (Scalar UI)
See the API Reference for endpoint details.
API Versioning
The /v1/ prefix allows for future breaking changes without affecting existing clients. When breaking changes are needed, a new /v2/ can be introduced while keeping /v1/ available. Currently, the API is stable and no version bumps are planned.
Service Layer
Business logic is separated into service modules:
services/images/- Image CRUD, queries, cleanupservices/albums/- Album managementservices/upload/- File validation and processingservices/encryption/- AES-256-GCM encryptionservices/webhooks/- Webhook deliveryservices/analytics/- Usage metrics
This keeps route handlers thin and logic reusable.
Data Layer
Database (D1)
SQLite database managed by Drizzle ORM. Schema includes:
- Users, images, albums, tags
- API keys with scope-based permissions
- Favourites, image variants
- Webhooks, audit logs
- Auth.js session tables
- Billing/subscription info
All queries are parameterized to prevent SQL injection. See the Database Reference for full schema.
Object Storage (R2)
Images are stored in R2 buckets with this structure:
{owner}/{album}/{imageId}/{variant}.{ext}Example: alice/vacation/img_abc123/w1024.webp
R2 provides:
- S3-compatible API
- No egress fees (when accessed via Workers)
- Automatic replication across regions
- Custom metadata support
Cache (KV)
Used for:
- Rate limiting state (with 90% write reduction via memory caching)
- Tenant Master Keys (encrypted)
- Custom domain verification cache
- CDN variant cache keys
KV is eventually consistent but very fast (< 5ms reads globally).
Analytics (Analytics Engine)
Time-series data for:
- Request counts per image/variant
- Bandwidth usage
- Geographic distribution
- Referrer tracking
See Analytics Configuration for setup details.
CDN Serving
The CDN handles public image delivery with on-the-fly transformations.
Request Flow
graph TD
A[GET /:owner/:album/:filename] --> B{Variant?}
B -->|None| C[Serve Original]
B -->|w1024| D[Check R2 Cache]
D -->|Hit| E[Serve Cached]
D -->|Miss| F[Generate Transform]
F --> G[Store in R2]
G --> E
B -->|Custom ?w=| H[Rate Limit Check]
H -->|OK| I[Transform & Cache]
H -->|Exceeded| J[429 Error]Image Variants
Pre-defined variants are generated asynchronously via queues:
thumb- 256x256 square thumbnailw128tow2048- Responsive widthsog-image- Open Graph preview
Custom transformations are supported with query parameters like ?w=500&h=300&fit=cover&format=webp. See Image Transforms for available options.
Caching Strategy
- Original images: Immutable (1 year cache)
- Variants: Immutable (1 year cache)
- Custom transforms: 24 hour cache
- 404 responses: 5 minute cache
Custom transforms are rate-limited per IP to prevent abuse (see Rate Limiting).
Authentication Flow
Pixelflare supports multiple authentication modes configured via AUTH_MODE:
graph TD
A[Incoming Request] --> B{Auth Strategy}
B -->|Bearer Token| C[API Key or JWT]
B -->|Cookie| D[Auth.js Session]
B -->|CF-Access-Jwt| E[Cloudflare Access]
C --> F[Verify & Extract Owner]
D --> F
E --> F
F --> G{Valid?}
G -->|Yes| H[Set c.get auth]
G -->|No| I[401 Unauthorized]Authentication Modes
- Cloudflare Access - Enterprise Zero Trust with JWT validation
- Auth.js - OAuth providers (GitHub, Google) for self-hosted
- API Keys - Bearer tokens with scopes for programmatic access
- No-Auth - Development mode only
The tryAuthenticate() middleware tries strategies in order. See Authentication Configuration for setup details.
Authorization
Every endpoint verifies ownership before accessing resources. The pattern:
const auth = c.get('auth');
const image = await getImage(db, auth.owner, imageId, ...);This prevents users from accessing each other's data. See Security Best Practices for details.
Processing Pipeline
Asynchronous work is handled by Cloudflare Queues with these consumers:
Image Processing Queue
- Trigger: After image upload
- Tasks: Generate variants (thumbnails, responsive sizes, WebP)
- Retries: 3 attempts with exponential backoff
Webhook Queue
- Trigger: On image create/update/delete events
- Tasks: Deliver webhook payloads with HMAC signatures
- Retries: 3 attempts
Backup Queue
- Trigger: Manual or scheduled
- Tasks: Sync images to external S3 (see S3 Backups)
- Retries: 3 attempts
Custom Domain Queue
- Trigger: Domain verification or certificate renewal
- Tasks: Update Cloudflare DNS and SSL
- Retries: 3 attempts
See Queue Configuration for tuning batch sizes and timeouts.
Scheduled Jobs
Cron triggers run these jobs:
- 01:00 UTC - Cleanup expired/deleted images
- 02:00 UTC - Analytics aggregation
- 03:00 UTC - S3 backup sync (if configured)
Schedules are defined in wrangler.toml. Jobs are idempotent and can be safely retried.
Deployment Architecture
Everything runs on Cloudflare's global network:
graph TB
U[Users Worldwide] --> CF[Cloudflare Edge]
CF --> FE[Pages: Frontend]
CF --> API[Workers: API]
CF --> CDN[Workers: CDN]
API --> D1[D1 Database]
API --> R2[R2 Storage]
API --> KV[KV Cache]
API --> AE[Analytics Engine]
API --> Q[Queues]
CDN --> R2
CDN --> KVWhy Cloudflare?
- Global edge network - 300+ data centers
- Zero cold starts - Workers use V8 isolates, not containers
- Integrated services - D1, R2, KV, Queues all co-located
- No egress fees - R2 accessed via Workers is free
- Generous free tier - Viable for small deployments
See Deployment Guide for setup instructions.
Build and Deployment Pipeline
The build process uses:
- Vite - Bundles frontend with hot module replacement
- Wrangler - Deploys Workers and manages secrets
- pnpm - Manages monorepo dependencies and workspaces
- GitHub Actions - CI/CD for automated deployments
Deployments are triggered via pnpm deploy which builds and deploys all packages in order. See GitHub Actions for automated deployment setup.
Key Design Decisions
Why Monorepo?
Shared types and configs reduce duplication. Changes to the API types automatically update the frontend. Single version bump for releases.
Why Cloudflare Workers?
Traditional CDNs require complex cache invalidation. Workers let us compute on every request with zero cold starts, enabling dynamic features like custom transforms and encrypted images.
Why Drizzle ORM?
Type-safe queries catch errors at compile time. Migrations are plain SQL, giving full control. No reflection or runtime overhead.
Why Zod for Validation?
Runtime type safety bridges TypeScript and user input. Schemas generate OpenAPI docs automatically. Transformers let us sanitize input inline.
Why Separate Service Layer?
Thin route handlers make testing easier. Service functions are reusable across HTTP routes, queue consumers, and cron jobs. Clear separation of concerns.
Why Token Bucket Rate Limiting?
Allows brief bursts while preventing sustained abuse. Memory caching reduces KV writes by 90%. Fail-open behavior prevents cascading failures if KV is down.
Why AES-256-GCM for Encryption?
Authenticated encryption prevents tampering. The two-tier key hierarchy (TMK → CEK) allows per-user and per-image key management. Users can export key bundles for offline decryption. See Encryption for details.
Why Not a Traditional Database?
D1 is SQLite at the edge, giving single-digit millisecond query times globally. No connection pooling needed. For most use cases, it's faster than a PostgreSQL database in a single region.
Data Flow Examples
Image Upload Flow
sequenceDiagram
participant U as User
participant F as Frontend
participant A as API
participant R2 as R2 Storage
participant Q as Queue
participant W as Worker
U->>F: Select file
F->>A: POST /v1/images (metadata)
A->>A: Validate, check limits
A-->>F: {id, uploadUrl, token}
F->>A: PUT /upload/:id?token=X (file data)
A->>R2: Store original
A->>Q: Enqueue variant job
A-->>F: 200 OK
Q->>W: Process variants
W->>R2: Store thumb, w1024, etc
W-->>Q: Job completeCDN Serving Flow
sequenceDiagram
participant U as User
participant C as CDN Worker
participant R2 as R2 Storage
participant K as KV Cache
U->>C: GET /alice/photos/sunset.jpg/w1024
C->>K: Check variant cache
K-->>C: Cache miss
C->>R2: Check R2 for variant
R2-->>C: File not found
C->>R2: Fetch original
C->>C: Transform to w1024
C->>R2: Store variant
C->>K: Update cache
C-->>U: Serve imageObservability
Logging
Structured JSON logs with contextual information. See Logging for patterns.
Analytics
Request counts, bandwidth, errors tracked via Analytics Engine. Dashboard in the frontend shows usage over time. See Analytics.
Audit Logs
All user actions logged to database for compliance. Queryable via API. See Audit Log.
Error Tracking
Errors logged with stack traces (but sanitized in responses). Cloudflare provides basic error metrics. For production, consider integrating Sentry or similar.
Testing
The codebase uses Vitest for unit and integration testing. Tests live alongside the code they test (*.test.ts files). Coverage includes:
- Middleware (auth, CORS, rate limiting)
- Service layer functions
- Validation schemas
- Cryptographic functions
- Queue consumers
See Testing for running tests and writing new ones.
Scaling Considerations
Horizontal Scaling
Workers scale automatically with zero configuration. No instances to manage or connection pools to tune.
Database Scaling
D1 handles ~50k reads/second per database with eventual consistency. For write-heavy workloads, consider batching writes or using KV for high-throughput data.
Storage Scaling
R2 has no size limits. Costs are ~$0.015/GB/month for storage. Consider lifecycle rules to delete old variants if storage costs become significant.
Cost Optimisation
- Use R2 instead of S3 to avoid egress fees
- Cache aggressively (variants are immutable)
- Batch queue jobs to reduce invocations
- Use KV memory caching to reduce writes
- Set appropriate retention periods for analytics and audit logs
See Cloudflare Pricing for current rates.
Development Workflow
For day-to-day development:
pnpm devruns frontend and API with hot reloadpnpm testruns the test suitepnpm type-checkvalidates TypeScript across all packagespnpm lintchecks code style
See Development Workflow for the full development process, including committing, testing, and deploying changes.
Further Reading
- Local Setup - Development environment
- Development Workflow - Daily development process
- Security Best Practices - Security patterns
- Testing - Writing and running tests
- Translations - Adding languages
- API Reference - Endpoint documentation
- Deployment Guide - Production setup
- Database Reference - Schema details
- Environment Variables - Configuration options
- Configuration Constants - Shared config values