Skip to content

Repository files navigation

Cloudflare Media Manager

Cloudflare Media Manager

A self-hosted, single-Worker app to browse, upload, edit, and manage all your Cloudflare media — Images, Stream videos, and audio in R2 — from one gallery. SSR UI on TanStack Start, API on Hono, all on a single Cloudflare Worker, gated by Cloudflare Access. Your Cloudflare API token is stored encrypted at rest and never reaches the browser.

License: AGPL-3.0   Cloudflare Workers   TanStack Start   TypeScript   Tests   PRs welcome


Screenshot

Cloudflare Media Manager — the unified media gallery in grid view

The unified library (grid view) — images, videos, and R2 audio with inline players, populated from your connected Cloudflare account.


What it does

Connect your Cloudflare account once (a scoped API token, entered in-app and stored AES-GCM-encrypted in D1) and manage everything from a unified library:

  • Unified library — Images, Stream videos, and R2 audio in one place, with Grid / Table / Cinema views, type filter, and live sort.
  • Upload — drag-and-drop images, videos (resumable TUS), and audio. Bytes go browser → Cloudflare via one-time, pre-authorized URLs; large files never transit the Worker.
  • Manage — rename, edit metadata, toggle signed-URL protection, delete (single + bulk).
  • Image transforms — an in-drawer builder for Cloudflare flexible variants (resize / fit / crop / rotate / quality / format / blur / sharpen / …) with a live preview, copy-URL, and download. HEIC uploads supported.
  • Image variants — full CRUD on account-level named variants (in Settings).
  • Video clipping — trim a Stream video into a new clip with a range slider.
  • Downloads — generate and download MP4 / audio-only M4A for a video.
  • Captions — list, AI-generate, upload .vtt, and delete subtitles.
  • Thumbnail & playback — set a video's poster-thumbnail timestamp (live preview) and allowed embedding origins.
  • Audio — first-class audio type stored in a dedicated R2 bucket, with an animated spectrum player.
  • Private by default — the whole app sits behind Cloudflare Access; the stored API token is used only server-side.

Tech stack

Layer Choice
Runtime Cloudflare Workers (workerd), single origin
UI / SSR TanStack Start (file-based routes, edge SSR) + React 19 + Vite
Components Mantine 9 (+ @gfazioli/mantine-audio, Tabler icons, Nunito)
Client state TanStack Query (server cache) + Zustand (UI state)
Tables TanStack Table + Virtual
API Hono, mounted as a splat server route on the same Worker
Auth gate Cloudflare Access JWT verification (jose)
Storage Cloudflare Images, Stream, R2 (audio), D1 (connection + audio metadata) + Drizzle
Secrets Stored CF token encrypted with AES-GCM (TOKEN_ENC_KEY)
Tooling oxlint + Prettier, Vitest, Wrangler 4

Architecture

One Cloudflare Worker serves the SSR UI and the API:

  • TanStack Start renders the UI at the edge; the Hono API is mounted as a catch-all server route (src/routes/api/$.tsapp.fetch(request, env)), so there's one origin, one wrangler deploy, no CORS.
  • accessGuard middleware verifies the Cloudflare Access JWT on every /api/* request.
  • Your Cloudflare API token is entered in the in-app Settings page and stored AES-GCM-encrypted in D1 (cf_connection). The browser only ever receives one-time upload URLs and signed delivery URLs — never the token.
  • Audio bytes live in a dedicated R2 bucket (AUDIO_BUCKET); audio metadata lives in D1 (audio_files).

Prerequisites

  • Node 22+ and a Cloudflare account (Workers Paid is recommended for Stream/Images).
  • Wrangler (npm i -g wrangler or use npx wrangler), logged in (wrangler login).
  • A scoped Cloudflare API token with Images Read+Edit and Stream Read+Edit (you'll paste this into the app's Settings, not into config).
  • A Cloudflare Access (Zero Trust) self-hosted application protecting the Worker's hostname.

Setup

git clone https://github.com/harborline/cloudflare-media-manager.git
cd cloudflare-media-manager
npm install

1. Create the D1 database and R2 bucket

wrangler d1 create media-gallery        # copy the returned database_id
wrangler r2 bucket create media-gallery-audio

Open wrangler.jsonc and fill in the placeholders:

  • d1_databases[0].database_id → the id from wrangler d1 create.
  • vars.TEAM_DOMAINhttps://<your-team>.cloudflareaccess.com.
  • vars.POLICY_AUD → your Access application's AUD tag (see step 3).
  • (Optional) add a routes entry to serve on your own domain.

Apply migrations (local + remote):

wrangler d1 migrations apply media-gallery --local
wrangler d1 migrations apply media-gallery --remote

2. Set the encryption secret

The stored Cloudflare token is encrypted at rest with an AES-GCM key:

wrangler secret put TOKEN_ENC_KEY      # paste the output of: openssl rand -base64 32

3. Protect it with Cloudflare Access

In Zero Trust → Access → Applications, add a self-hosted application for the Worker's hostname (e.g. media.example.com or <worker>.<subdomain>.workers.dev) plus a policy for who may sign in. Then copy:

  • your team domain → TEAM_DOMAIN,
  • the application AUD tag → POLICY_AUD.

4. Deploy

npm run deploy        # vite build && wrangler deploy

Visit the deployed URL, sign in through Access, open Settings, and paste your scoped Cloudflare API token + account ID to connect.

5. (Optional) CI auto-deploy

.github/workflows/deploy-app.yml deploys on push to main when these repository secrets are set (the deploy step is skipped otherwise, so CI stays green without them):

  • CLOUDFLARE_API_TOKEN
  • CLOUDFLARE_ACCOUNT_ID

Local development

cp .dev.vars.example .dev.vars     # sets TOKEN_ENC_KEY + DEV_BYPASS_ACCESS=1
npm run dev                        # http://localhost:5173

DEV_BYPASS_ACCESS=1 lets you run locally without Cloudflare Access. The local D1 starts empty — connect your account via Settings to populate the gallery.

Scripts

Command What it does
npm run dev Vite dev server (UI + Worker via the Cloudflare plugin)
npm run build Build the client + SSR server bundles
npm run deploy Build and wrangler deploy
npm run typecheck tsc --noEmit for the app and the worker
npm run test Vitest (worker + pure-logic unit tests)
npm run check oxlint --fix + Prettier

Project structure

src/                      # TanStack Start UI
  routes/                 # file-based routes (incl. api/$.ts → Hono)
  features/Gallery.tsx    # the unified media library
  components/             # cards, table, cinema, detail drawer + edit panels,
                          # upload modal, transform/clip/download/caption/settings panels
  lib/                    # cf-api fetchers, media model, zustand store, helpers
worker/                   # Cloudflare Worker (server)
  src/routes/             # Hono routes: images, stream, audio, settings, me, …
  src/middleware/access.ts# Cloudflare Access JWT verification
  src/lib/                # cf REST helpers, URL/sign helpers, connection + audio stores
  migrations/             # D1 migrations
docs/                     # standalone docs site (React + Mantine + MDX)

Security model

  • Every /api/* route is gated by Cloudflare Access (JWT verified server-side with jose).
  • The Cloudflare API token is stored encrypted (AES-GCM) in D1 and used only inside the Worker.
  • Uploads use one-time, pre-authorized Cloudflare URLs; private delivery URLs are HMAC-signed server-side.
  • No account credentials are ever sent to the browser.

Contributing

Issues and PRs welcome. Run npm run check && npm run typecheck && npm run test before opening a PR.

License

GNU AGPL-3.0-or-later. Source files carry /* AGPL-3.0-or-later */ headers.

About

Self-hosted gallery & editor for Cloudflare Images, Stream, and R2 audio — single Worker (TanStack Start SSR + Hono), behind Cloudflare Access.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages