VERT is the kind of self-hosted tool that should exist more often: a useful web app where your server mostly just serves the interface, and your browser does the work.

It is a file converter for images, audio, documents, and video workflows. The main privacy pitch is simple: non-video files are converted on your device using WebAssembly instead of being uploaded to a random cloud converter.

VERT is an open-source file conversion utility built with Svelte and TypeScript, with WebAssembly-powered local conversion for many formats.

What is VERT?

VERT is a browser-first file conversion web app.

The app supports image, audio, document, and video conversion flows. Under the hood it uses browser-side WebAssembly builds of tools like ImageMagick, FFmpeg, and Pandoc.

There is one important exception: video conversion is not treated like normal lightweight browser conversion. VERT uses a separate service called vertd for video jobs. The hosted VERT instance can use hosted vertd; a privacy-focused self-hosted deployment should either run its own vertd or disable external requests.

Why Self-Host VERT?

Self-host VERT if you want a cleaner, private alternative to ad-heavy file conversion websites.

Good reasons:

  • Local-first conversion: images, audio, and documents can be processed in the browser.
  • No account system: the main app has no users, auth, or database.
  • Simple hosting: it builds to static files and runs behind Nginx.
  • Fast homelab fit: one container is enough for the web UI.
  • Configurable external services: Plausible, donation, Stripe, and hosted vertd calls are controlled at build time.

It is especially useful on a LAN or private tunnel: open the page, drag a file in, convert it, download the result.

Tech Overview of VERT

I inspected the repository at commit 063383e685dee61531f71c77d41443c5a04bd872. The GitHub API did not report a published release, so the Home-Lab compose pins the inspected commit instead of relying on a release tag.

The stack is straightforward:

  • Frontend: Svelte 5 and SvelteKit 2.
  • Language: TypeScript.
  • Build tool: Vite.
  • Package manager: Bun.
  • Static adapter: @sveltejs/adapter-static.
  • Runtime image: nginx:stable-alpine.
  • Image conversion: @imagemagick/magick-wasm.
  • Audio conversion: @ffmpeg/ffmpeg.
  • Document conversion: pandoc.wasm with a WASI shim.
  • Video conversion: optional external vertd.

The converter registry lives in src/lib/converters/index.ts. It wires ImageMagick, FFmpeg, Pandoc, and, unless external requests are disabled, vertd.

That split matters. When PUB_DISABLE_ALL_EXTERNAL_REQUESTS=true, VERT removes hosted video conversion behavior from the UI, but the project FAQ notes that FFmpeg’s WebAssembly build is still downloaded from cdn.jsdelivr.net.

Architecture Notes

VERT does not need a database because the main application is static.

The Docker image is a two-stage build:

  1. Bun installs dependencies and runs bun run build.
  2. Nginx serves the generated static files.

The conversion path is mostly client-side:

  • the uploader stores selected files in browser state;
  • VERT chooses a converter based on the input file format;
  • Web Workers isolate conversion work from the UI thread;
  • WASM-backed tools process the file;
  • the browser returns a downloadable output file.

For documents, Pandoc runs through a browser WASI shim. For images, ImageMagick runs in a worker. For audio, FFmpeg writes files into its virtual filesystem and reads the converted output back.

Video is different: vertd handles upload, WebSocket progress, conversion, cancellation, and download.

Self-Hosting VERT with Docker

VERT publishes a GHCR image, but there is a catch: public PUB_* settings are baked into the static build. The upstream Docker docs warn that if you pull the prebuilt image, you cannot change environment variables such as analytics or vertd URL at runtime.

For that reason, the Home-Lab compose below builds from the inspected Git commit and passes privacy-focused build args.

Docker Compose Configuration

The reusable Home-Lab compose file is here:

The site can include the same snippet from the Home-Lab submodule:

assets/snippets/vert/docker-compose.yml
includeyaml: file not found or not allowed by security.filesystem.allow: assets/snippets/vert/docker-compose.yml

Create .env from the sample:

cp assets/snippets/vert/.env.sample assets/snippets/vert/.env

Build and start it:

cd assets/snippets/vert
docker compose up -d --build

Open:

http://localhost:3000

Because the default sample uses:

PUB_DISABLE_ALL_EXTERNAL_REQUESTS=true

the app is built without hosted Plausible, donation, Stripe, and hosted vertd behavior.

Field Note: Docker Trial on This Host

I tested the published GHCR image locally:

docker run -d --name vert-foss-trial \
  -p 127.0.0.1:18080:80 \
  ghcr.io/vert-sh/vert:latest

Results:

  • the image pulled successfully;
  • the image manifest supported linux/amd64 and linux/arm64;
  • HEAD / returned 200 OK;
  • Nginx served VERT’s static index.html;
  • headers included X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy;
  • the container healthcheck became healthy;
  • the trial container was removed after validation.

This confirms the web UI serves correctly. It does not prove that every browser-side converter works on every machine, because real conversion depends on browser WASM support, available memory, file size, and the specific format pair.

Video Conversion and vertd

If you need video conversion privately, plan for vertd.

The VERT docs describe vertd as a separate FFmpeg wrapper built in Rust. It runs an HTTP service on port 24153 by default. The VERT UI can point at that URL from Settings.

For a same-machine test, the UI setting can be:

http://localhost:24153

For a shared web deployment, remember that localhost means the user’s browser machine, not your Docker host. If several users access VERT through a domain, put vertd behind a reachable URL and configure CORS/TLS appropriately.

Browser FFmpeg Limits

VERT does run FFmpeg in the browser for its browser-side audio/media converter. That happens through @ffmpeg/ffmpeg and FFmpeg WebAssembly.

That does not mean you get the same capability profile as native FFmpeg on a server.

The browser path is good for common local conversions such as MP3, WAV, FLAC, OGG, Opus, AAC, M4A, and similar audio workflows. It can also understand some video containers when the job is audio-oriented.

But browser FFmpeg has real limits:

  • it is slower than native FFmpeg;
  • it is constrained by browser memory and ArrayBuffer limits;
  • it does not have hardware acceleration such as NVENC, VAAPI, or Quick Sync;
  • it may not include every codec, filter, muxer, demuxer, protocol, or build option you expect from a full native FFmpeg install;
  • large video jobs are a poor fit.

That is why VERT uses vertd for serious video conversion. If you need full FFmpeg-style video transcoding, run vertd or another native FFmpeg service. Treat the browser FFmpeg path as a convenient local converter, not as a complete replacement for the FFmpeg CLI.

Privacy Notes

The safe mental model is:

  • images: browser-side;
  • documents: browser-side;
  • audio: browser-side, with FFmpeg WASM fetched from a CDN;
  • video: external vertd unless disabled or self-hosted.

For a privacy-first deployment, build your own image with:

PUB_DISABLE_ALL_EXTERNAL_REQUESTS=true
PUB_PLAUSIBLE_URL=
PUB_VERTD_URL=
PUB_DONATION_URL=
PUB_STRIPE_KEY=

That is what the generated Home-Lab sample does by default.

When to Pick VERT

Pick VERT when you want a clean self-hosted replacement for generic web file converters.

It is a strong fit for:

  • homelab utility dashboards;
  • private family or team tools;
  • quick image/document/audio conversion;
  • deployments where a static app is preferable to a server with accounts and storage;
  • users who understand that very large conversions still depend on browser memory.

It is not a full media transcode farm by itself. For serious video conversion, pair it with vertd or use a dedicated media processing tool.

FAQ

Is VERT a PWA?

Yes, with a practical caveat.

VERT ships a web app manifest at /manifest.json with display: standalone, app icons, theme color, and a / start URL. It also registers /sw.js as a service worker, so browsers can treat it like an installable web app when served over HTTPS.

The service worker is not a full offline-app-shell strategy, though. It focuses on caching conversion-heavy assets:

  • /pandoc.wasm;
  • FFmpeg core JS and WASM from cdn.jsdelivr.net;
  • production worker JS assets;
  • ImageMagick WASM paths.

So I would describe VERT as an installable web app with targeted WASM caching, not as a guaranteed fully offline PWA.

Does VERT use Vite?

Yes.

The build script is:

"build": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide && vite build"

The project uses SvelteKit with Vite, @sveltejs/vite-plugin-svelte, vite-plugin-wasm, and vite-plugin-top-level-await.

Can VERT be deployed statically to Cloudflare Pages?

Yes, the main VERT web app is a good fit for Cloudflare Pages because it uses @sveltejs/adapter-static and outputs a static build/ directory.

A Cloudflare Pages setup should look like this:

Build command: bun run build
Build output directory: build
Root directory: repository root

Set the PUB_* variables as build-time environment variables in Cloudflare Pages, not runtime variables.

For a privacy-first Pages deployment:

PUB_ENV=production
PUB_HOSTNAME=your-domain.example
PUB_DISABLE_ALL_EXTERNAL_REQUESTS=true
PUB_DISABLE_FAILURE_BLOCKS=true
PUB_PLAUSIBLE_URL=
PUB_VERTD_URL=
PUB_DONATION_URL=
PUB_STRIPE_KEY=

With those settings, the static app should work for image and document conversion, and audio conversion should work when the browser can fetch and load FFmpeg WASM. The main limitation is video: full private video conversion requires a reachable self-hosted vertd endpoint, and that endpoint is not part of a plain static Cloudflare Pages deployment.

Does Cloudflare Pages provide full VERT functionality?

For the main static app, yes.

For the whole VERT ecosystem, not by itself.

Cloudflare Pages can serve the SvelteKit static files, manifest, service worker, worker bundles, and bundled WASM assets. That covers the web UI and browser-side conversion model.

But Cloudflare Pages will not run the separate vertd video daemon. If you want video conversion, run vertd somewhere else and expose it at a URL the user’s browser can reach. Then build VERT with PUB_VERTD_URL set to that URL and PUB_DISABLE_ALL_EXTERNAL_REQUESTS=false.

Does VERT use WASM or Pyodide?

VERT uses WebAssembly directly. I did not find Pyodide in the dependency graph or source.

The relevant pieces are:

  • @imagemagick/magick-wasm for image conversion;
  • @ffmpeg/ffmpeg plus FFmpeg core WASM for audio/media conversion;
  • /pandoc.wasm with @bjorn3/browser_wasi_shim for document conversion;
  • vert-wasm for helper parsing such as ICNS handling.

So the short answer is: WASM, not Pyodide.

Final Thoughts

VERT is small operationally but interesting technically.

The core app is just static files, yet it brings serious conversion engines into the browser through WebAssembly. That makes it easy to self-host, easy to reverse proxy, and easy to keep private for common file conversions.

The only part I would be strict about is build configuration. Do not pull latest and assume the runtime .env will change privacy behavior. Build the image with the public settings you actually want, then serve it like any other static app.