A hybrid CDN using Cloudflare Workers and R2 storage. Supports file upload/browsing with multi-client organization, GitHub proxy with versioning/minification, direct R2 file serving, and a programmatic Objects API (PUT|GET /api/objects) with a shared TypeScript client (packages/client β @dxd/cdn) for any DXD project.
- π€ Web-based file upload with password protection
- π File browser with fuzzy search and filters (client/project/env)
- π File analytics tracking (request count, first/last served)
- ποΈ File deletion via API
- π¬ MP4 streaming with range request support
- π Live URLs revalidate in the browser (republish without a hard refresh)
- π¦ Objects API +
@dxd/cdnclient (and optionalCdnObjectsWorker binding)
- π Global CDN via Cloudflare's edge network
- π¦ Serves files from public and private GitHub repositories
- π·οΈ Version control support (releases and commit hashes)
- π Automatic minification for JS and CSS files
- πΎ R2 caching for improved performance
- ποΈ Automatic compression for script/link requests
- π JSDelivr URL conversion support
- π Pre-caching option for faster delivery
src/
index.js # Main entry point, request routing
config/
constants.js # Content types, preview types, GitHub config, cache policies
handlers/
api.js # /api/files, /api/file-stats, β¦
browse.js # File browser UI
objects-api.js # PUT|GET /api/objects (HTTP adapter)
responses.js # R2 and GitHub response handling
streaming.js # MP4 streaming support
upload.js # File upload handling
services/
cdn-objects.js # CdnObjects WorkerEntrypoint (service binding)
github.js # GitHub API integration
minification.js # Basic JS/CSS minification
objects.js # R2 put/get (no HTTP statuses)
templates/
browse.js # Browse page HTML templates
pages.js # Special pages (speed-test, convert)
upload.js # Upload page HTML templates
utils/
cache.js # Public GET Cache-Control / ETag / 304
compression.js # Gzip compression utilities
cors.js # CORS handling utilities
files.js # File operations, fuzzy search, analytics
r2.js # Key fallback + conditional GET
packages/
client/ # @dxd/cdn TypeScript client
- Create a Cloudflare account if you don't have one
- Install Wrangler CLI:
npm install -g wrangler
- Login to Cloudflare via Wrangler:
wrangler login
-
Create an R2 bucket in Cloudflare Dashboard:
- Go to R2 section
- Click "Create bucket"
- Name it
dxd-cdn(or update wrangler.toml if using a different name)
-
Update wrangler.toml with your bucket details (already configured if using default name)
# Required for file upload/browse authentication
wrangler secret put UPLOAD_PASSWORD
# Required for GitHub proxy functionality
wrangler secret put GITHUB_TOKENFor the GitHub token:
- Go to GitHub.com β Settings β Developer Settings β Personal Access Tokens
- Select scopes:
public_repo(public only) orrepo(private repos)
-
Add your domain in Cloudflare Dashboard:
- Go to Workers & Pages β Select your worker β Add Custom Domain
-
Update wrangler.toml:
routes = [ { pattern = "your-domain.com", custom_domain = true } ]
npm install
npm run deployVisit https://your-domain.com/upload to:
- Upload files with password authentication
- Specify upload path (client/project/env structure recommended)
- Auto-generates unique filenames if conflicts exist
Visit https://your-domain.com/browse to:
- Browse all uploaded files
- Search with fuzzy matching
- Filter by client, project, or environment
- View file analytics
- Delete files
- Copy CDN URLs
https://your-domain.com/[client]/[project]/[env]/[filename]
Example:
https://your-domain.com/acme/website/prod/hero-image.webp
Query parameters:
?download=true- Force download instead of inline display
Public GET honors the object's stored Cache-Control. Unchanged files return 304 on If-None-Match or If-Modified-Since.
Programmatic publish/pull for Studio, Heard, client Workers, and CI. Prefer this over rclone so each object gets a Cache-Control.
PUT /api/objects
Authorization: Bearer <UPLOAD_PASSWORD>
X-DXD-Object-Key: heard/hp/prod/personalization.js
X-DXD-Cache-Control: public, max-age=0, must-revalidate # omit for this default
X-DXD-Cache-Control allowlists two values (anything else is 400):
| Use | Value |
|---|---|
Live pointer (config.json, personalization.js, /upload) |
public, max-age=0, must-revalidate (PUT default) |
| Hashed / versioned snapshot | public, max-age=31536000, immutable |
Shared client: packages/client (@dxd/cdn) β putObject, publishHashedAsset (snapshot then live), publishVersioned. Same-account Workers can bind CdnObjects instead of sending the password over HTTP; that binding can write any key in the bucket.
Headers and Worker recipe: docs/api-objects.md, docs/connect-a-worker.md.
https://your-domain.com/[repo-name]/[version]/[file-path]
Where version can be:
- A release tag:
v1.0.0 - A commit hash:
a1b2c3d - Latest release:
latest
Examples:
# Specific version
https://your-domain.com/my-project/v1.0.0/dist/script.js
# Minified version (add .min before extension)
https://your-domain.com/my-project/v1.0.0/dist/script.min.js
# Latest release
https://your-domain.com/my-project/latest/dist/script.js
Visit https://your-domain.com/convert for a web interface to:
- Convert GitHub URLs to CDN URLs
- Convert JSDelivr URLs to CDN URLs
- Select versions or commit hashes
- Toggle minification
- Pre-cache files
Auth: Authorization: Bearer <UPLOAD_PASSWORD> or ?password= (same secret as /upload). JSON APIs send Cache-Control: no-store.
| Endpoint | Method | Description |
|---|---|---|
/api/objects |
PUT | Store an object (X-DXD-Object-Key, optional X-DXD-Cache-Control / X-DXD-Overwrite) |
/api/objects |
GET | Authenticated meta or body (?key= and as=meta or as=body) |
/api/files |
GET | List files with optional search/filter |
/api/file-stats |
GET | Get analytics for a specific file |
/api/file-content |
GET | Get HTML file content |
/api/delete-file |
DELETE | Delete a file |
/api/files
search- Fuzzy search queryclient- Filter by client nameproject- Filter by project (format:client/project)env- Filter by environment (stagingorprod)
/api/file-stats and /api/file-content
file- Full file path
/api/delete-file
file- Full file path to delete
/api/objects
key- Object key (orX-DXD-Object-Keyon PUT)as-meta(default) orbodyon GET
npm run dev # Start local dev server
npm run deploy # Deploy to Cloudflare| Variable | Required | Description |
|---|---|---|
UPLOAD_PASSWORD |
Yes | Password for upload/browse/API access |
GITHUB_TOKEN |
For GitHub proxy | GitHub Personal Access Token |
ENVIRONMENT |
No | Set to "production" in prod |
PUBLIC_ORIGIN |
No | Public origin in RPC/service-binding URLs (HTTP handlers use the request origin). Production: https://cdn.designxdevelop.com |
Recommended structure for uploaded files:
/:client/:project/:env/:filename
Examples:
acme/website/prod/logo.svgacme/website/staging/hero-video.mp4bigcorp/landing-page/prod/styles.css
- GitHub releases cached 5 minutes in-memory
- Hashed / versioned assets (and GitHub
/:repo/:version/:file): 1 yearimmutable - Live objects (
config.json,personalization.js, web uploads):public, max-age=0, must-revalidateon browser and Cloudflare cache headers β no timed edge copy. Next navigation revalidates (304if unchanged) - PUT
/api/objectsallowlists only those twoCache-Controlstrings - API JSON responses use
no-store
After deploying this Worker, republish existing live keys. Overwriting R2 updates new visitors. Browsers that already stored the URL as immutable will not recheck until they drop that entry β those clients need a new URL (hashed/versioned filename) or an explicit cache purge.
See docs/api-objects.md and docs/connect-a-worker.md.
- Minification only supported for JS and CSS files (basic, Workers-compatible)
- R2 storage limits based on your Cloudflare plan
- GitHub API rate limits apply when fetching new files
- Private repos require
reposcope GitHub token