A temporary file hosting service
  • Svelte 48%
  • JavaScript 28.8%
  • TypeScript 16.7%
  • CSS 5.6%
  • Dockerfile 0.4%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-15 22:25:11 +02:00
client chore: update default retention to 1h 2026-09-15 22:25:11 +02:00
server feat: E2EE + text mode 2026-09-15 17:21:13 +02:00
.dockerignore Initial version 2026-09-15 14:15:43 +02:00
.env.example feat: E2EE + text mode 2026-09-15 17:21:13 +02:00
.gitignore Initial version 2026-09-15 14:15:43 +02:00
docker-compose.yml feat: E2EE + text mode 2026-09-15 17:21:13 +02:00
docker-entrypoint.sh feat: E2EE + text mode 2026-09-15 17:21:13 +02:00
Dockerfile feat: E2EE + text mode 2026-09-15 17:21:13 +02:00
package-lock.json Initial version 2026-09-15 14:15:43 +02:00
package.json Initial version 2026-09-15 14:15:43 +02:00
README.md feat: E2EE + text mode 2026-09-15 17:21:13 +02:00

Fuse

A small, self-hosted temporary file host. Drop files, get a link and a QR code, pick how long it lives. When the fuse runs out, the files are gone.

  • No accounts. Every transfer gets a private uploader key instead. The uploader link is the share link followed by #KEY (mega.nz style): open it and you get a Delete button. The key never leaves the browser as part of the URL — it is sent as a header.
  • Retention presets: 1 hour, 6 hours, 1 day, 3 days, 1 week, 2 weeks, 1 month, or one-time download (each file burns after its first complete download, and the whole transfer still expires after 90 days at most).
  • Robust transfers. Uploads are chunked (8 MiB) with a tus-style offset handshake: a failed chunk is rolled back server-side and retried with backoff, and the client re-syncs its offset from the server, so a flaky connection or a proxy hiccup never corrupts a file. Downloads support HTTP ranges (except one-time files, which are only marked used once the full response has been sent). "Download all" streams an uncompressed zip.
  • Text mode. Paste text instead of files; recipients see it rendered inline with a copy button. Same lifetimes, and "one-time" burns it after the first view.
  • End-to-end encryption (optional). Turn it on and pick a password (8+ characters, or hit Generate): files, their names and text are encrypted in the browser with AES-256-GCM (key via PBKDF2, 600k iterations, per-transfer salt). The server stores only ciphertext and an encrypted manifest; the share link reveals nothing without the password. Recipients decrypt in their browser, which is why encrypted transfers are capped at 200 MB and download file by file (no zip).
  • Single SQLite file + one directory per transfer on disk. Files are stored under opaque ids, always served as application/octet-stream attachments.

Run

npm install
npm run dev        # server on :3000 + Vite on :5180 (proxied)
npm run build && npm start
npm test           # server API tests (node:test)

Docker

cp .env.example .env   # optional
docker compose up -d --build

Data (SQLite + files) lives in the /data volume (./data by default).

Behind a reverse proxy

Set TRUST_PROXY=true (the compose file does) and make sure the proxy allows request bodies of at least MAX_CHUNK_BYTES (64 MiB by default; the browser sends 8 MiB chunks). For nginx:

client_max_body_size 64m;
proxy_request_buffering off;
proxy_read_timeout 3600s;

Configuration

Everything is via environment variables — see .env.example. Notable ones:

Variable Default Meaning
MAX_TRANSFER_BYTES 2 GiB total size of one transfer
MAX_FILES 20 files per transfer
E2EE_MAX_BYTES 200 MiB total size of an end-to-end encrypted transfer
TEXT_MAX_BYTES 1 MiB size of a text paste
ONE_TIME_MAX_DAYS 90 hard expiry for one-time transfers
STALE_UPLOAD_HOURS 24 abandoned (never finalized) uploads are dropped after this
TRUST_PROXY false trust X-Forwarded-* from a reverse proxy

API

Method Path Auth Purpose
GET /api/config — limits + retention presets
POST /api/transfers — {duration, kind?, e2ee?, salt?} → {id, key, expiresAt, oneTime, kind, e2ee}
POST /api/transfers/:id/files key register a file {name,size,mime}
PATCH /api/transfers/:id/files/:fid key append a chunk (Upload-Offset header, raw body)
GET /api/transfers/:id/files/:fid key current offset (for resume)
POST /api/transfers/:id/finalize key publish the transfer ({manifest?} for encrypted ones)
GET /api/transfers/:id optional key transfer + files (owner: true with a valid key)
DELETE /api/transfers/:id key delete everything
GET /d/:id/:fid/:name — download one file (?k=KEY = owner, doesn't consume one-time files)
GET /z/:id/:name.zip — download all as zip (not for encrypted transfers)

The key goes in the X-Fuse-Key header (or ?k= for download links).

Layout

server/   Express + better-sqlite3 (app.js routes, store.js data access, db.js schema)
client/   Svelte 5 + Vite; bespoke design tokens in src/app.css (light/dark)