
Tiny desktop apps for macOS, Windows and Linux: a txiki.js backend + a native webview window.
.app bundles out of build, auto-update
out of publishQuestions, help, or something you built: come say hi on Discord.
curl -fsSL https://tinyjs.app/install | sh
The same script now handles Linux too (it detects the OS). Installs to
~/.tinyjs and symlinks tinyjs onto your PATH. Pin a version with
TINYJS_VERSION=vX.Y.Z. Later, tinyjs update runs the copy of the
installer that came with your install if a newer release exists (--check
only reports, --dry-run shows what it would run); tinyjs dev
also mentions new releases, checking at most once a day. After updating,
run tinyjs update-project in each project to refresh its types and
coding-agent skill (see below). Linux needs the
system WebKitGTK runtime: sudo apt install libwebkit2gtk-4.1-0 on
Debian/Ubuntu, sudo zypper install libwebkit2gtk-4_1-0 on openSUSE.
Prebuilt binaries ship for Linux x86_64 and arm64 with the
first tagged release after Linux support merged — the installer says so
plainly against older releases.
To install from source instead:
git clone https://github.com/tarwin/tinyjsapp && cd tinyjsapp
./setup.sh # downloads the txiki.js runtime, compiles the launcher
ln -s "$(pwd)/tinyjs" /usr/local/bin/tinyjs
On Linux, setup.sh needs the system dev packages first. Debian/Ubuntu:
sudo apt install build-essential pkg-config libgtk-3-dev
libwebkit2gtk-4.1-dev libayatana-appindicator3-dev. openSUSE:
sudo zypper install gcc-c++ make pkgconf-pkg-config gtk3-devel
webkitgtk3-devel libayatana-appindicator3-devel (WebKitGTK 4.1's dev
package is webkitgtk3-devel there — the "3" says GTK3, not an older
WebKit). It
downloads a prebuilt tjs from the tinyjsapp releases, or builds txiki.js
from source (TJS_BUILD=1 ./setup.sh, needs cmake + ninja).
Windows:
irm https://tinyjs.app/install.ps1 | iex
Installs prebuilt binaries to %LOCALAPPDATA%\tinyjs (override with
TINYJS_HOME; pin with TINYJS_VERSION), x86_64 or native arm64 to match
the machine (Windows on ARM gets the x86_64 build, which runs emulated, from
releases before 0.62.0), and adds it to your user PATH —
open a new terminal afterwards. Needs only the WebView2 runtime
(preinstalled on Windows 11). tinyjs update runs the installer's bundled copy.
(Requires a release that ships Windows assets — the first one after Windows
support merged; the installer says so plainly against older releases.)
To develop tinyjs itself from source (needs MinGW-w64:
winget install BrechtSanders.WinLibs.POSIX.UCRT; on Windows on ARM,
winget install MartinStorsjo.LLVM-MinGW.UCRT for a native launcher —
setup.ps1 fetches an arm64 tjs.exe from the latest release):
git clone https://github.com/tarwin/tinyjsapp; cd tinyjsapp
powershell -ExecutionPolicy Bypass -File setup.ps1
# adds the checkout to your user PATH (-SkipPath to opt out); after that,
# `tinyjs dev` auto-rebuilds the launcher whenever the native sources change
Linux:
Runs on X11 and Wayland sessions — Ubuntu 24.04+ and current distros with
webkit2gtk-4.1. Use the same install command above (it detects Linux) or
build from source with ./setup.sh (see the distro deps above). Needs the
WebKitGTK 4.1 runtime — libwebkit2gtk-4.1-0 on Debian/Ubuntu,
libwebkit2gtk-4_1-0 on openSUSE.
Playing media? WebKitGTK decodes through GStreamer, and a stock desktop
install carries only some of it. Without the rest, <audio>/<video> play
MP3, Ogg/Vorbis, Opus, WAV and FLAC, but AAC/M4A won't play at all
(canPlayType('audio/mp4; codecs="mp4a.40.2"') returns "") and Media
Source Extensions report every type unsupported — which covers most podcasts,
most internet radio, and most adaptive streaming. Add:
# Debian/Ubuntu
sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav
# openSUSE
sudo zypper install gstreamer-plugins-bad gstreamer-plugins-ugly gstreamer-plugins-libav
WebKit hints at this itself, logging "WebKit wasn't able to find a WebVTT encoder … unless gst-plugins-bad is installed" on startup.
Don't play audio through Web Audio on Linux. WebKitGTK renders the Web
Audio graph on a normal-priority (SCHED_OTHER) thread while its media threads
get real-time priority, so anything reaching ctx.destination misses its
deadline and crunches — on an idle machine, at any latencyHint, whether the
source is an element or a decoded buffer. A plain <audio> element goes
through GStreamer instead and is flawless. There is no graph-side fix; buffering
only softens it, and rtkit won't promote the thread.
So on Linux, play the element directly and get analysis from tiny.audioTap:
if (tiny.system.isLinux()) {
audio.volume = volume; // straight to the speakers, no graph
await tiny.audioTap.start({ scope: 'app' }); // PCM for your visualiser
} else {
const src = ctx.createMediaElementSource(audio);
src.connect(gain); gain.connect(ctx.destination);
}
scope: 'app' captures only your own output (a private PipeWire null sink fed
by your app's ports), so playback is unaffected and you don't hear the rest of
the desktop. What you lose is anything that was a graph node — an equalizer,
a StereoPanner — so tell the user rather than leaving dead controls on screen.
For sound effects none of this applies: tiny.audio.sampler mixes decoded
samples with per-voice volume/pan/pitch natively in the launcher on Linux
(PipeWire's real-time data path — clean under any page load) and via Web Audio
on macOS/Windows, same API, same equal-power pan numbers. Use it instead of
createBufferSource graphs.
See examples/amp for the whole pattern, visualisers included.
Analysis-only graphs are fine: a MediaElementSource feeding nothing but an
AnalyserNode never reaches destination, so a missed deadline costs a
dropped frame rather than audible crackle. Keep such elements at volume = 0,
since WebKitGTK also plays a graph-routed element's own output straight to the
speakers (macOS/Windows mute it).
See Portability below for what's supported on Windows and Linux.
If something doesn't work, run tinyjs doctor (from your project folder,
if you have one). It checks the install, which tinyjs your PATH runs, the
system webview, Node tooling and the project's tinyjs.json, and says how to
fix what it finds. It only reads files: see docs/docs/doctor.md.
Full docs: tinyjs.app/docs · release history: tinyjs.app/changelog.
tinyjs new myapp
cd myapp
tinyjs dev
A window opens. dev hot-reloads: edit anything in src/frontend/ and the
page re-renders in place, caches bypassed (no restart, backend state
survives); edit backend sources (the backend entry's folder) and the
process restarts automatically. With a dev server (devUrl) the server's
own HMR does the frontend instead; a watch-mode tool that writes into the
frontend folder and reloads the page itself sets "frontend": { "reload":
false } so the page doesn't reload twice.
TINYJS_DEBUG=1 tinyjs dev traces every message crossing the bridge.
Frameworks welcome: tinyjs new myapp --template react-ts (or vue-ts,
svelte-ts, solid-ts, preact-ts, lit-ts, alpine-ts, vanilla-ts, …) scaffolds a
Vite app wired to tinyjs — tinyjs dev runs Vite's dev server with HMR inside
the native window, and tinyjs build ships the built assets as usual. Plain
--template asks which framework and language; --pm npm|pnpm|yarn|bun|vp
picks the package manager (otherwise you choose from the ones that work on
your machine, or get npm when there's no terminal to ask on), and
dependencies install straight away unless --no-install. The chosen manager
runs the project's dev and build scripts ("dev": "pnpm run dev" in
tinyjs.json). --pm vp uses Vite+ 1.0 or newer
over another manager (vp:pnpm, vp:bun, …; asked if left out); an older
vp is refused, since its projects can't build against current Vite+
packages (vp upgrade). alpine is Vite's vanilla template with
Alpine.js and a small component that calls the
backend. TypeScript backends are bundled with esbuild automatically (which
also makes npm packages usable in the backend). The zero-dependency default
scaffold is unchanged.
The tiny global is injected into every page by the launcher — no script
tag needed — and ships with full TypeScript definitions (types/tiny.d.ts).
New projects also get a tinyjs reference skill for coding agents
(.claude/skills/tinyjs/ and .agents/skills/tinyjs/). Both are copied
in when the project is made, so after updating tinyjs, run
tinyjs update-project in the project to bring them up to date
(tinyjs doctor says when they're behind). It overwrites edits to those
files, and adds the skill to a project that doesn't have one. For any
other AI tool, the docs are at https://tinyjs.app/llms.txt (an index of
markdown pages) and https://tinyjs.app/llms-full.txt (all in one file).
A project is just:
``
myapp/
tinyjs.json # { name, title, size, id, version, icon?, signIdentity?,
# urlScheme? (string or array), fileExtensions?, chrome?, update?, notarize?,
# permissions? ({ microphone, camera, speechRecognition, location: "why" }),
# audioTap? ("app" | "system"), audioTapReason? (macOS prompt text),
# contextMenu? (false suppresses WebKit's default right-click menu),
# debug? (true = F12 opens devtools (⌥⌘I also works on macOS),
# "open" = every window auto-opens them; default off —tinyjs dev` always has F12),
# browserAccelerators? (true re-enables the engine's own keys —
# Ctrl+F find, Ctrl+R reload…; default suppressed, Windows only),
| 日期 | 榜单 | 排名 | 周期新增 | 总 STAR |
|---|