A private media workspace for Cloudflare and Docker with multiple storage backends, guest image hosting, file management, and secure sharing.
中文 | English
Warning
Do not deploy the checked-in Wrangler configuration unchanged after forking. Replace project and Worker names, every KV ID, R2 bucket name, Durable Object script_name, WebAuthn domain/origin, and migration audience in both configuration files. Deploy the Coordinator before Pages.
Important
Production must keep authentication enabled and store secrets as Secrets. AUTH_DISABLED=true is local-only. Coordinator, KV, or encryption-key failures are explicit; the service does not degrade to legacy credentials or plaintext configuration.
This tree started from katelya77/K-Vault — a Telegram-first serverless media vault on Cloudflare with R2, S3, Discord, Hugging Face, WebDAV, and GitHub backends (CC0-1.0). The shared skeleton is still visible: the root pages, functions/, server/, the kvault_data volume, and the same four workflows. K-Vault's own acknowledgements credit cf-pages/Telegraph-Image as an earlier upstream reference, and this tree's first commit still carried the Telegraph-Image README plus a fork-sync workflow pointed at it.
The upstream deployment path is kept here for readers arriving from K-Vault, for a minimal Telegram-only deployment, and for comparing behaviour. This fork has diverged — the coordinator Worker, Passkey login, and Storage Profiles described below are not upstream — so deploy from the sections after this one and treat the upstream steps as background.
- Fork and connect. Fork K-Vault, then open the Cloudflare dashboard → Workers & Pages → Create → Pages → Connect to Git and select the fork.
- Leave the build settings empty. Framework preset
None; root directory, install command, build command, build output directory, and deploy command all empty. Do not putnpx wrangler deploythere — that is a Workers command, and Pages answers withThe detected framework ("Hono") cannot be automatically configured. Do not point the output directory atfrontend/disteither; upstream publishes the repository root as-is. - Bind KV. Create a KV namespace, then add a namespace binding to the Pages project with the variable name
img_url. Image management, chunked upload tasks, and base configuration all depend on it. - Configure at least one storage backend under Settings → Environment variables. Telegram is the shortest path:
TG_Bot_TokenandTG_Chat_ID, plus optionalBASIC_USERandBASIC_PASSfor the admin console. R2, S3, Discord, Hugging Face, WebDAV, and GitHub are alternatives; redeploy after any variable change, because a running deployment never re-reads configuration. - Verify. Open
/api/statusand confirm thetelegram,kv, andr2blocks report what you configured. - Optional direct upload. Without Git integration:
npm run pages:deploy -- --project-name <PAGES_PROJECT>, which isnpx wrangler pages deploy .. Never mix a Direct Upload project with a Git-integrated one.
docker volume create kvault_data
docker run -d --name kvault --restart unless-stopped -p 8080:8080 \
-v kvault_data:/app/data ghcr.io/katelya77/k-vault:latestThe admin console is at /admin.html, WebDAV at /webdav.html, health at /api/health. On first start the container generates CONFIG_ENCRYPTION_KEY and SESSION_SECRET into /app/data/runtime.env inside the volume; removing the container keeps them, removing the volume does not. With a clone, docker compose up -d pulls the published image and reads an optional .env copied from .env.example (PUBLIC_BASE_URL, DEFAULT_STORAGE_TYPE, UPLOAD_MAX_SIZE, CHUNK_SIZE, WEB_PORT). Storage backends can also be added, tested, and switched from the console after startup.
| K-Vault (upstream) | This repository |
|---|---|
| Repository root published as static pages, Pages Functions only | Vue app built into frontend/dist and uploaded with wrangler pages deploy frontend/dist, plus a coordinator Worker hosting the AUTH_COORDINATOR and UPLOAD_COORDINATOR Durable Objects |
TG_Bot_Token, TG_Chat_ID |
TG_BOT_TOKEN, TG_CHAT_ID, plus dedicated TG_GUEST_BOT_TOKEN and TG_GUEST_CHAT_ID for guest uploads |
Admin login with BASIC_USER / BASIC_PASS |
Password plus Passkey/WebAuthn (WEBAUTHN_RP_ID, WEBAUTHN_ORIGIN, MIGRATION_AUDIENCE); AUTH_DISABLED is refused outside local runs |
Default backend chosen by DEFAULT_STORAGE_TYPE plus an environment block |
Also multi-instance Storage Profiles managed from the console |
Published image ghcr.io/katelya77/k-vault:latest |
Built from source with docker compose up -d --build after npm run docker:init-env |
| CC0-1.0 | MIT, see LICENSE |
Telegram-side prerequisites are unchanged, and upstream's /api/status, /admin.html, and /webdav.html entry points still exist here. Everything specified below — the coordinator, Passkey, API tokens, Storage Profiles, and the wrangler.jsonc replacement list — belongs to this fork.
- Upload images, media, and documents with URL upload, chunked/multipart paths, and SSRF protection.
- Telegram, R2, S3, Discord, Hugging Face, WebDAV, and GitHub storage adapters.
- Password and Passkey/WebAuthn login, scoped API tokens, short links, and signed private shares.
- Folder tree, filtering, batch move/delete, rename, visibility controls, and previews.
- Isolated Telegram bot/channel, fixed quotas, and content authenticity checks for public guest image hosting.
- Stable Legacy UI:
/,/login.html,/admin.html,/gallery.html,/webdav.html,/storage-settings.html. - Vue UI:
/app/,/app/drive,/app/storage,/app/status; both runtimes implement its Storage, Drive, and Share APIs.
| Upload | Gallery | Administration | Passkey |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
flowchart LR
browser[Browser / API client] --> frontend[Legacy + Vue UI]
frontend --> runtime{Runtime}
runtime --> pages[Pages Functions]
runtime --> docker[Docker Hono API]
pages --> coordinator[Auth + Upload Coordinator DO]
pages --> metadata[Workers KV metadata]
docker --> metadata_docker[SQLite / Redis metadata]
coordinator --> r2[R2 multipart storage]
pages --> adapters[Storage adapters]
docker --> adapters
adapters --> remote[Telegram / S3 / WebDAV / others]
Cloudflare coordinates authentication and R2 multipart state with Durable Objects and stores metadata in KV. Docker runs Hono with SQLite, an optional Redis settings store, and persistent volumes. The UI is shared; persistence and adapter implementations differ.
| Goal | Choose | Shortest path |
|---|---|---|
| Global edge, serverless | Cloudflare Pages + Worker | Create KV/R2/DO, edit configs, deploy Worker then Pages |
| VPS, NAS, local private hosting | Docker Compose | Generate .env, replace secrets, build and start |
npm ci
npm run frontend:build
npm run docker:init-env
docker compose up -d --build
npm run docker:doctorDevelopment and the Docker backend require Node.js 22+. Never commit .env, real tokens, passwords, or encryption keys.
- Create a Pages project, KV namespace, R2 bucket, and a Worker that hosts both Durable Objects.
- Replace every repository-specific value in
wrangler.jsoncandworkers/coordinator/wrangler.jsonc:
| Placeholder | Replace in |
|---|---|
<PAGES_PROJECT> |
Pages name |
<COORDINATOR_WORKER> |
Worker top-level and env.production.name, plus both DO script_name values |
<KV_NAMESPACE_ID> |
Every KV ID in both files, including env.production |
<R2_BUCKET> |
Every bucket name in both files, including env.production |
<YOUR_DOMAIN> |
WEBAUTHN_RP_ID and the HTTPS WEBAUTHN_ORIGIN |
<MIGRATION_AUDIENCE> |
Current KV audience, normally your KV namespace ID |
- Configure
BASIC_USER,BASIC_PASS,SESSION_SECRET,CONFIG_ENCRYPTION_KEY, andFILE_SHARE_SECRET_CURRENTas production Pages Secrets. Add main-storage credentials as needed and dedicatedTG_GUEST_BOT_TOKENplusTG_GUEST_CHAT_IDfor guests. Use plain variables for WebAuthn RP/origin; keep binding namesimg_url,R2_BUCKET,AUTH_COORDINATOR, andUPLOAD_COORDINATOR. - Preserve existing R2 lifecycle rules. Add multipart cleanup with a read-add-read sequence; never use an operation that replaces the complete ruleset:
npx wrangler r2 bucket lifecycle list <R2_BUCKET>
npx wrangler r2 bucket lifecycle add <R2_BUCKET> abort-incomplete-uploads multipart/ --abort-multipart-days 1
npx wrangler r2 bucket lifecycle list <R2_BUCKET>
npx wrangler deploy --config workers/coordinator/wrangler.jsonc --env production
npm run frontend:build
npx wrangler pages deploy frontend/dist --project-name <PAGES_PROJECT> --branch main
node scripts/probe-coordinator-binding.mjs --base-url https://<YOUR_DOMAIN>Do not reverse the deployment order. The lifecycle rule only aborts incomplete uploads under multipart/ after one day; it does not remove completed objects. Preserve any all-prefix default rule and every unrelated rule. npm run pages:deploy consumes the checked-in configuration, so a fork must not run it before completing every replacement.
npm run docker:init-env
docker compose up -d --build
npm run docker:doctor
docker compose logs apiReplace every example secret in .env before startup. SQLite data and configuration persist in kvault_data by default. Set SETTINGS_STORE=redis for external Redis; the Compose Redis service starts through its profile. A reverse proxy must provide HTTPS, and PUBLIC_BASE_URL plus WebAuthn RP/origin must match the public domain. See the English Docker guide.
| Backend | Cloudflare | Docker | Hard ceiling |
|---|---|---|---|
| R2 | direct / multipart | direct / chunked | Admin setting, 100 MiB default maximum |
| S3, WebDAV | direct / streaming | direct / chunked | Admin setting, 100 MiB default maximum |
| Telegram | direct | direct / chunked | 20 MiB on Cloudflare/guest; 50 MiB for Docker admin |
| Discord | direct | direct / chunked | 25 MiB |
| Hugging Face | direct | direct / chunked | 35 MiB |
| GitHub | direct | direct / chunked | Admin setting, 100 MiB default maximum |
The direct threshold is 20 MiB; only Cloudflare R2 uses multipart. Admin-saved dynamic storage settings are AES-GCM encrypted: Cloudflare writes them to KV, while Docker storage profiles always use SQLite; Redis is only an optional, separate general settings store. Dynamic settings take precedence over environment variables, and a blank secret field preserves its current value. Wrangler/the Cloudflare dashboard still controls the native R2 binding.
One storage type may have multiple named profiles, with exactly one enabled default per type. Administrator uploads bind both storageMode and the exact storageId. Disabled profiles reject new writes but remain available for historical reads, deletion, and migration. Environment values are bootstrap or explicit-migration inputs only; runtime resolution never falls back to old global credentials when a profile is missing, disabled, or unavailable.
A Cloudflare R2 profile selects either binding mode (a Wrangler binding such as R2_BUCKET) or s3 mode (endpoint, bucket, access key, and secret). Docker R2 uses S3-compatible credentials. Guest Channel remains isolated: guest callers cannot enumerate profiles or submit an administrator storageId.
Before upgrading v1/Legacy/SQLite state, run a dry-run and then use an environment-specific driver for backup, freeze, stage, activation, live verification, and marker commit. See the multi-storage migration rehearsal for exact commands, rollback rules, error meanings, and verified hashes.
- Authentication: Pages uses
AuthCoordinatorfor initialization, login-failure limits, and session state. Binding or persistence failures fail closed. Environment credentials are not a runtime fallback after initialization. - Passkeys: the RP ID is the domain and origin is its HTTPS origin. Register credentials again after changing domains.
- File visibility: Drive sources default to
private; image-host, API, and Legacy sources default topublic, while APIs may explicitly requestprivate. Private access uses expiring signatures and revocable leases. Guest files default topublic. - Guest uploads: both dedicated
TG_GUEST_*values are required. Missing values reject uploads instead of falling back to the administrator bot. Only AVIF/GIF/JPEG/PNG/WebP files whose signature, MIME, and extension agree are accepted. The fixed per-IP quota is 10/day, retention is at least one day, the configurable ceiling is 20 MiB, and the example default is 5 MiB. - Deletion semantics: guest metadata expiry invalidates project links but does not prove deletion of remote Telegram bytes. Apply a separate channel cleanup policy when remote deletion is required.
Phase two introduced no new paid third-party dependency. Cloudflare Pages, Workers, KV, R2, and Durable Objects can still incur usage-based charges; review the provider's current pricing and allowances before deployment.
| Responsibility | Variables or bindings |
|---|---|
| Authentication/encryption | BASIC_USER, BASIC_PASS, SESSION_SECRET, CONFIG_ENCRYPTION_KEY |
| Share signing | FILE_SHARE_SECRET_CURRENT, FILE_SHARE_SECRET_PREVIOUS, FILE_SHARE_SECRET_PREVIOUS_VALID_UNTIL |
| Passkeys | WEBAUTHN_RP_ID, WEBAUTHN_ORIGIN, WEBAUTHN_RP_NAME |
| Pages data | img_url, R2_BUCKET, AUTH_COORDINATOR, UPLOAD_COORDINATOR |
| Telegram | TG_BOT_TOKEN, TG_CHAT_ID, TG_GUEST_BOT_TOKEN, TG_GUEST_CHAT_ID |
| Docker data | DATA_DIR, DB_PATH, SETTINGS_STORE, SETTINGS_REDIS_URL, TRUST_PROXY |
| Optional backends | R2_*, S3_*, WEBDAV_*, DISCORD_*, HF_*, GITHUB_* |
See .env.example for the base Docker template. Public Docker guest uploads also need TG_GUEST_*, GUEST_RETENTION_DAYS, and TRUST_PROXY=true only behind a trusted reverse proxy. Environment values bootstrap settings; encrypted dynamic settings saved by an administrator take precedence. Errors remain explicit—do not assume automatic fallback.
UI routes /app/storage and /app/drive are not API routes. Every operation below requires administrator authentication:
| Scope | Method and path |
|---|---|
| Storage query/create | GET /api/storage/list, POST /api/storage |
| Storage mutation/test | PUT/DELETE /api/storage/:id, POST /api/storage/default/:id, POST /api/storage/:id/test, POST /api/storage/test |
| Drive query/folders | GET /api/drive/tree, GET /api/drive/explorer, POST /api/drive/folders, POST /api/drive/folders/move, DELETE /api/drive/folders |
| Drive files/sharing | POST /api/drive/files/move, POST /api/drive/files/rename, POST /api/drive/files/delete-batch, POST /api/share/sign |
curl --fail-with-body https://<YOUR_DOMAIN>/api/auth/check
curl --fail-with-body https://<YOUR_DOMAIN>/api/status
curl --fail-with-body -u '<ADMIN_USER>:<ADMIN_PASSWORD>' -F file=@example.png https://<YOUR_DOMAIN>/upload
curl --fail-with-body -u '<ADMIN_USER>:<ADMIN_PASSWORD>' https://<YOUR_DOMAIN>/api/storage/list
curl --fail-with-body -u '<ADMIN_USER>:<ADMIN_PASSWORD>' https://<YOUR_DOMAIN>/api/drive/tree
curl --fail-with-body -u '<ADMIN_USER>:<ADMIN_PASSWORD>' --json '{"fileId":"<FILE_ID>"}' https://<YOUR_DOMAIN>/api/share/signFor scripts, issue a scoped Bearer token in the admin UI and call /api/v1/*; never put a token in a URL. Protected APIs should return 401/403 when unauthenticated, not fabricated empty results.
npm run frontend:build
npm test -- --reporter dot
npm audit --omit=dev
npm run test:storage-e2e
npm run test:auth-e2e
npm run test:visualManifest ranges are support declarations, lockfiles record current exact resolutions, and security reports record versions used by a completed verification; they are not interchangeable. Browser E2E requires Playwright browsers and its runtime. See the phase-two security report for production evidence.
| Symptom | Root cause and action |
|---|---|
COORDINATOR_BINDING_* / login 5xx |
Worker not deployed first, mismatched script_name, or missing DO migration; compare both configs and run the binding probe |
NO_ENC_KEY |
Set CONFIG_ENCRYPTION_KEY or SESSION_SECRET; never save plaintext credentials |
GUEST_STORAGE_NOT_CONFIGURED |
Set both guest bot token and chat ID |
| Incomplete multipart data | List rules, add the one-day abort rule, and list again; do not replace existing lifecycle rules |
| Passkey origin/RP error | RP is the domain only; origin is the full HTTPS origin; both must match the browser address |
| Docker refuses startup | Replace example passwords/keys, check volume permissions, and inspect docker compose logs api |
STORAGE_PROFILE_NOT_FOUND / STORAGE_NOT_WRITABLE |
Check the exact storageId, type, and enabled state; do not fall back to global credentials |
STORAGE_PROFILE_IN_USE |
Files, chunk tasks, or lifecycle operations still reference the profile; finish or reconcile them first |
MIGRATION_ACTIVATION_AMBIGUOUS |
Keep the Cloudflare freeze and Docker lock; inspect authority/ledger evidence before recovery |
Key directories: frontend/ (Vue and Legacy build), functions/ (Pages Functions), workers/coordinator/ (DO Worker), server/ (Docker API), shared/ (cross-runtime contracts), test/ and e2e/ (verification), and docs/ (deployment/security reports). See Cloudflare Pages + R2 and the Chinese Docker guide.
The project introduction (中文) summarises the current goals, the main route flow, the technical structure, and what has been removed from this version.
Update both language documents in the same change, run the build and relevant tests, and never commit build output, real credentials, or account-specific configuration. Origin: started from katelya77/K-Vault (CC0-1.0), which credits cf-pages/Telegraph-Image as an earlier upstream reference; this tree is released under the MIT License.



