Your media. Your space.
A fast, private, self-hostable media workspace. Paste a link, pick a format, save it offline. Built with plain HTML, CSS and ES modules on the front end, and Node.js and FFmpeg on the back end. No frontend build step. Two runtime dependencies.
Just want to use it? Download the desktop app for Windows, macOS (Apple Silicon) or Linux from the latest release. No setup needed.
Want to contribute? Pick a good first issue, say hi in Discussions, and open a PR. Setup takes about a minute:
npm ci && npm run setup && npm run dev.
Features Β· Screenshots Β· Desktop app Β· Quick start Β· Configuration Β· Architecture Β· Security Β· Deploy Β· Contributing
47-second product demo. Watch the full-quality MP4.
- Three ways to find media: paste a single link, paste a batch of links, or search by keyword.
- Metadata first, download second. Flux resolves the real title, creator, duration and available resolutions before you commit to anything.
- Video (MP4) or audio (MP3), with a resolution cap (only resolutions the source really offers, no fake upscaling) and MP3 bitrates of 128/192/256/320 kbps.
- Subtitle embedding and language selection where the source provides them.
- Playlists become individual jobs, each validated separately.
- Live queue with real progress. When total bytes are unknown the progress is
null. Flux never invents a percentage. - Cancel, retry, regenerate and clear actions, all scoped to the owner.
- Resumable downloads through HTTP range requests on the file endpoint.
- Automatic cleanup: finished files expire after a configurable retention window (24 h by default), while history records stay and offer one-click regeneration.
| Guest | Signed in with Google | |
|---|---|---|
| Inspect and search links | β | β |
| Download genuine 50% previews | β | β |
| Full-length downloads | β (401 at the API) |
β |
| Private download library | per-browser | per account |
| Guest records carry over on sign-in | n/a | β |
Guest previews are produced by FFmpeg on the server. Flux probes the real duration of the downloaded source, re-encodes from the beginning to exactly half of the measured runtime, runs FFprobe on the output, and fully decodes it to catch corruption. Only a validated file is ever offered for download, and the full-length source is deleted straight after.
Paste a link and the whole workspace re-themes to match the platform (surface, type, controls and colour tokens) with a single bottom-up colour sweep. TikTok and X get a dark treatment, the rest get distinct light ones. Mixed batches and unknown URLs keep the Flux default, and the sweep is disabled for users who prefer reduced motion.
YouTube Β· TikTok Β· Instagram Β· X / Twitter Β· SoundCloud Β· Pinterest Β· Vimeo Β· Dailymotion Β· Twitch Β· RadioJavan Β· Reddit Β· Facebook Β· Bilibili Β· Archive.org Β· Wikimedia
These are conditional source families powered by the media engine's extractors, not individually guaranteed integrations. Platforms change often. Operators can add reviewed hosts with
FLUX_EXTRA_SOURCE_HOSTS.
- Semantic HTML, responsive from 1440 px down to 320 px, an auto-collapsing desktop rail and a mobile drawer.
- Keyboard-first: skip link, a
/quick-access shortcut, managed dialog focus and full tab order. - Automated axe-core WCAG A/AA checks on every page and the auth dialog at 1440, 390 and 320 px.
- Respects
prefers-reduced-motion.
Screenshots are generated from fixture data with node scripts/screenshots.mjs, so they never show real user content.
Don't want to run a server? Flux also ships as a desktop app:
| System | Download |
|---|---|
| Windows | Flux-Setup-*.exe |
| macOS (Apple Silicon) | Flux-*.dmg |
| Linux | Flux-*.AppImage |
Get them from the latest release, open the app, paste a link and save. Full-length downloads work without Google sign-in, and the first launch needs an internet connection to fetch its media engine.
The builds are unsigned, so Windows SmartScreen (More info, then Run anyway) and macOS Gatekeeper (right-click the app, then Open) show a warning the first time. Developer notes are in desktop/.
Desktop app troubleshooting
- Windows says "Windows protected your PC": click More info, then Run anyway.
- macOS says the app can't be opened: right-click (or Control-click) the app, choose Open, then confirm. If it still refuses, go to System Settings, Privacy & Security and click Open Anyway.
- The macOS download only supports Apple Silicon (M1 or newer). Intel Macs can run Flux from source (see Quick start).
- Linux: make the AppImage executable first:
chmod +x Flux-*.AppImage. - "Flux could not start": the first launch needs internet to fetch its media engine. Connect and try again.
- Downloads that used to work now fail: sites change often. The app updates its media engine each time it starts, so restart it. If it still fails, open an issue and pick Desktop app problem.
- Where your data lives: Windows
%APPDATA%Flux, macOS~/Library/Application Support/Flux, Linux~/.config/Flux. Deleting that folder resets the app.
Requirements: Node.js 22.9+ (24 LTS recommended). npm run setup downloads the media engine for you (checksum-verified) and, on Windows x64, FFmpeg/FFprobe too. On macOS/Linux install FFmpeg with your package manager (brew install ffmpeg, apt install ffmpeg).
git clone https://github.com/10k11/flux.git
cd flux
npm ci
npm run setup # fetches the media engine (and FFmpeg on Windows) into ./bin
npm startOpen http://localhost:3000. Guest previews work immediately, with no Google credentials needed.
cp .env.example .env # optional: tune limits, enable Google sign-in
npm run dev # auto-restarting dev servercp .env.example .env # set FLUX_PUBLIC_HOST=your.domain
docker compose up -d --buildThis starts Flux, an internal PO-token provider for YouTube, and Caddy, which terminates TLS and obtains certificates automatically. See Deployment.
All configuration is through environment variables (loaded from .env by npm start).
| Variable | Default | Purpose |
|---|---|---|
FLUX_ORIGIN |
http://localhost:3000 |
Canonical public origin. HTTPS required outside localhost. No trailing slash. |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
none | Enables Google sign-in. Server-side only. |
FLUX_PUBLIC_HOST |
none | Domain used by Docker Compose and Caddy. |
FLUX_PUBLIC_OPEN |
0 |
1 = open to the internet (rate limits apply). |
FLUX_ACCESS_PASSWORD / FLUX_ACCESS_USERNAME |
none / flux |
Optional HTTP Basic gate for private deployments. |
FLUX_MAX_CONCURRENT |
2 |
Simultaneous jobs on the whole server. |
FLUX_MAX_FILESIZE |
4G |
Largest single file fetched. |
FLUX_MAX_DURATION |
7200 |
Maximum source duration in seconds. |
FLUX_MAX_JOB_BYTES |
4294967296 |
Aggregate temp bytes per running job. |
FLUX_RETENTION_HOURS |
24 |
How long finished files are kept. |
FLUX_INSPECT_PER_10MIN |
40 |
Link lookups per visitor per 10 minutes. |
FLUX_JOBS_PER_HOUR |
15 |
Jobs started per visitor per hour. |
FLUX_ACTIVE_PER_VISITOR |
6 |
Queued + running jobs per visitor. |
FLUX_TRUST_PROXY |
0 |
Trust X-Forwarded-For. Enable only behind a proxy that overwrites it. |
FLUX_EXTRA_SOURCE_HOSTS |
none | Extra reviewed source hostnames (comma-separated). |
FLUX_DATA_DIR |
./data |
Private state: jobs, sessions, temp media. |
YTDLP_PATH, FFMPEG_PATH |
auto | Override tool locations. |
Setting up Google sign-in
Flux uses Google's server-side OAuth authorization-code flow with the official google-auth-library. No Supabase or separate auth service is needed.
-
In Google Cloud β Google Auth Platform, configure the consent screen (name, support email, audience; add test users while in testing).
-
Create an OAuth client of type Web application.
-
Register the exact redirect URI:
- Local:
http://localhost:3000/auth/google/callback - Production:
https://YOUR_DOMAIN/auth/google/callback
- Local:
-
Set the server-only variables:
FLUX_ORIGIN=https://YOUR_DOMAIN GOOGLE_CLIENT_ID=your-web-client-id GOOGLE_CLIENT_SECRET=your-client-secret
-
Restart Flux, then sign in, refresh, sign out and test cancellation.
Use one canonical hostname: localhost and 127.0.0.1 do not share cookies. Without credentials the sign-in dialog says so honestly and shows no fake button.
flowchart LR
B["Browser<br/>(plain ES modules)"] -->|"cookie session + X-Flux-Token"| S["Node HTTP server<br/>server.mjs"]
S --> A["lib/auth.mjs<br/>Google OAuth, sessions"]
S --> P["lib/platforms.mjs<br/>source allowlist"]
S --> M["lib/media.mjs<br/>format selection, validation"]
S -->|spawn| Y["media engine"]
Y -->|"all HTTP(S)"| E["lib/egress.mjs<br/>loopback egress proxy<br/>DNS pin, public IPs only"]
E --> I(("Internet"))
S -->|spawn| F["FFmpeg / FFprobe<br/>preview + validation"]
S --> D[("FLUX_DATA_DIR<br/>jobs.json, sessions.json,<br/>per-job temp dirs")]
The download pipeline:
Source input β URL validation β metadata β selection β verified session β owned job β extraction β preview / full processing β private file delivery β history
flux/
βββ server.mjs HTTP server, routing, job queue, rate limits
βββ lib/
β βββ auth.mjs Google OAuth (PKCE, state, nonce), sessions, CSRF
β βββ egress.mjs SSRF-safe loopback proxy for extractor traffic
β βββ media.mjs engine argument builder, format rules, validation
β βββ platforms.mjs Supported source families
βββ public/ Static front end (no bundler)
β βββ index.html Β· styles.css Β· app.js Β· api.js Β· components.js Β· icons.js
βββ scripts/ setup, syntax check, screenshots, icon generation
βββ test/ node:test unit/integration + Playwright + axe suites
βββ docs/screenshots/ README images
βββ Dockerfile Β· compose.yaml Β· Caddyfile
βββ .github/ CI, issue forms, PR template, Dependabot
Design decisions:
- Zero build step.
public/is served as-is. Edit, refresh, done. - Two runtime dependencies:
google-auth-libraryandipaddr.js. Everything else is the Node standard library. - Single process, single volume. JSON state with atomic replacement keeps self-hosting trivial. Don't run multiple instances on one data directory; move to a database and shared queue before scaling horizontally.
All POST endpoints require the session's X-Flux-Token CSRF header.
| Endpoint | Purpose |
|---|---|
GET /api/session |
Guest/account session, CSRF token, Google config status, retention |
GET /auth/google Β· /auth/google/callback |
Start / complete OAuth |
POST /api/logout |
Revoke session, issue a fresh guest session |
POST /api/inspect |
{url, playlist?} or {search} β real metadata/results |
POST /api/jobs |
{items:[{url}], mode:"preview"|"full", authorized:true, options} |
GET /api/jobs |
The current owner's jobs only |
POST /api/jobs/:id/cancel Β· /retry |
Cancel an active job / regenerate a failed or expired one |
GET /api/jobs/:id/files/:index |
Private, cookie-authenticated file stream (range-enabled) |
POST /api/jobs/clear |
Remove the owner's finished records and files |
GET /api/health |
Actual media engine / FFmpeg availability |
GET /api/platforms |
Source-family directory |
options: format (mp4/mp3), quality (best or a resolution cap), bitrate (128/192/256/320), subtitles, language.
Job states: queued, processing, downloading, completed, failed, cancelled, expired.
Flux is designed to be exposed to the internet, so security is a core feature, not an afterthought.
- Server-side OAuth with state + PKCE + nonce. The ID token's signature, audience, issuer, expiry and
email_verifiedare all checked. Onlyopenid email profileare requested, and Google access/refresh tokens are never stored. - Sessions: opaque tokens stored hashed,
HttpOnly,SameSite=Lax,Secureover HTTPS, rotated on login/logout, 7-day expiry, per-session CSRF tokens. - Authorization on every object: jobs, lists, retries, cancels, clears and file delivery are checked against the session owner. Full downloads require a verified account. Browser-supplied identity fields are ignored.
- SSRF protection: extractor traffic goes through a loopback-only egress proxy that validates DNS results and pins a public IP per connection, including redirects. Private, loopback, link-local, reserved and IPv4-mapped addresses are refused.
- Argument-injection safe: URLs are passed as separate
spawnarguments after--. No credentials or custom ports in source URLs, and no arbitrary extractor arguments are accepted. - FFmpeg sandboxing: preview processing is limited to local
file,dataandcryptoprotocols. - Resource limits: duration, per-job bytes, file size, concurrency, per-visitor and per-IP rate limits, and an aggregate disk guard.
- No public file serving: media is only reachable through authenticated streams, never held fully in memory.
Found a vulnerability? Please follow SECURITY.md and do not open a public issue.
cp .env.example .env
# set FLUX_PUBLIC_HOST, GOOGLE_CLIENT_ID/SECRET, and FLUX_PUBLIC_OPEN=1 (or a password)
docker compose up -d --build- Point your domain's A/AAAA record at the server (ports 80 and 443 open).
- Caddy obtains and renews TLS certificates automatically.
- Register
https://YOUR_DOMAIN/auth/google/callbackin Google Cloud. - The PO-token provider (
flux-pot) stays on the internal Docker network. Never publish its port. - The Flux container includes a Docker healthcheck for
/api/health; usedocker compose psto verify that it becomeshealthyafter startup.
Operator notes. YouTube may still block datacentre IPs or require extra checks, and a token provider is no guarantee. Keep the media engine and FFmpeg updated (rebuild the image regularly), apply container filesystem quotas, and add outbound firewall rules as defence in depth. The optional cookies file is sensitive and must never be served. The shared Basic-auth password is only an outer gate and does not grant full-download privileges.
npm run check # parse every module
npm test # unit + integration (incl. real FFmpeg tests)
npm run format:check # Prettier
# with the server running:
npx playwright install chromium
npm run test:browser # end-to-end workflow, responsive checks
npm run test:a11y # axe-core WCAG A/AA
node scripts/screenshots.mjs # regenerate README screenshotsWhat the suite covers: URL validation, format selection, session rotation/persistence/revocation, OAuth state/nonce verification, CSRF, guest-full denial, cross-owner isolation, blocked internal destinations, real FFmpeg preview generation (a 600 s video β ~300 s, a 240 s audio β ~120 s, fully decoded), 18 responsive page checks and axe scans.
Ideas welcome via issues. Current thinking:
- SQLite storage and a shared queue for multi-instance deployments
- Optional additional OAuth providers
- Thumbnails in lookup results and the library
- Light/dark preference toggle independent of source theming
- Internationalisation
- Prometheus metrics endpoint
Contributions of every size are welcome: bug reports, docs, tests, new ideas and code. Start with CONTRIBUTING.md and look for issues labelled good first issue. Everyone participating is expected to follow the Code of Conduct.
Contributors are welcome, human or AI. Coding agents should start with AGENTS.md (code map, commands, rules) and llms.txt. Work goes through forks and pull requests.
Flux is a tool. You are responsible for how you use it. Only save media you own, that is licensed for it, or that you have permission to download, and respect the terms of service of each platform and the copyright law where you live. Flux asks every user to attest to this on each request, but that attestation is not an automated copyright determination. The maintainers do not host, index or distribute any media, and do not endorse infringement. Spotify links are used only for metadata (to search a match); Flux never extracts Spotify audio.
FFmpeg Β· PO token provider Β· Caddy Β· Playwright Β· axe-core. Platform marks come from Simple Icons (CC0); the RadioJavan mark is from its official site. Trademarks belong to their respective owners.
MIT Β© Lokii. Built by lokii.dev.






