# tinyjs: native desktop apps in JavaScript without Electron or Node

> tinyjs is a small toolchain that packages a txiki.js JavaScript backend and a plain HTML frontend into a native desktop app for macOS, Windows and Linux, shipping about 6 MB and talking to the page over a Unix domain socket. Its Linux story is the interesting part, because the system WebKit is where the compromises live.

**tarwin/tinyjsapp** — Build native apps in JS. Backend, frontend. ~5Mb.

- Repository: https://github.com/tarwin/tinyjsapp
- Stars: 1,343 · Forks: 48
- Language: C
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/tarwin-tinyjsapp

## What tinyjs produces and who it is for

tinyjs is a toolchain for building small native desktop applications in JavaScript. The README describes the shape of the result: a txiki.js backend and a native webview window, shipped as about 6 MB in two real files, with no Electron, no Node and no bundled Chromium.

The audience is a developer who wants a desktop app without carrying a browser runtime. A menu bar utility, a tray application, a small tool for an internal workflow: the kind of program that needs native menus, notifications, drag and drop with real file paths, and a window, but does not need a web stack worth hundreds of megabytes. The README's feature list names exactly that set, including tray and menu-bar apps and window control.

The other audience member is someone with a small team and a short deadline, who would rather write one HTML frontend than maintain three native codebases. The toolchain ships templates, a CLI, a docs folder, a skill folder and a test folder, so the project is aimed at people who will build a project from it rather than read it.

The engineering bet is worth naming plainly. Instead of shipping its own renderer, tinyjs uses the webview the operating system already has, WebKit on macOS, WebKitGTK on Linux and WebView2 on Windows. That single decision is what produces the 6 MB figure, and it is also the source of every platform-specific wrinkle in the README.

## The architecture: a JavaScript backend and a page that never opens a port

The backend is plain JavaScript running on txiki.js, and the README is explicit that it has full system access: files, sockets, processes and FFI. The frontend is plain HTML, CSS and JavaScript, multi-file if you like, and it is rendered by the system WebKit. WebGPU is listed as available, which is a pleasant surprise for a system webview.

The transport between them is the part that sets tinyjs apart from the usual local-server arrangement. There is no HTTP server and no ports. The page and the backend talk over a Unix domain socket in a private temp directory. That removes a class of problems people usually accept with local web apps: no port to collide with, no firewall prompt, no listening socket visible to anything else on the machine, and nothing to bind to 127.0.0.1 for a local service that was never meant to be a service.

The distribution model follows from the same thinking. Hot reload in development, signed .app bundles out of build, and auto-update out of publish. The repository layout supports that: a template/ directory holds the project template, native/ the native launcher sources, runtime/ the runtime pieces, cli.js the command line, scripts/ the packaging scripts, and setup.sh and setup.ps1 the source installers for Unix and Windows.

The result is a toolchain rather than a framework. It gives you a project skeleton, a dev loop, a packaging step and an update channel. It does not give you a component library, a state manager or a router, and the frontend side is the web platform you already know.

## Installing tinyjs on macOS, Linux and Windows

On macOS and Linux the installer is a single line, and the README says it detects the operating system:

```sh
curl -fsSL https://tinyjs.app/install | sh
```

It installs to ~/.tinyjs and symlinks tinyjs onto your PATH. A specific version can be pinned with TINYJS_VERSION=vX.Y.Z, and later tinyjs update runs the copy of the installer that came with your install if a newer release exists, with --check to only report and --dry-run to show what it would run. The dev command mentions new releases too, checking at most once a day.

Linux is where the installer stops being enough, because the system WebKitGTK runtime has to be present. On Debian and Ubuntu that is one package, and on openSUSE the name differs:

```sh
sudo apt install libwebkit2gtk-4.1-0
sudo zypper install libwebkit2gtk-4_1-0
```

Building from source on Linux needs the development packages, and the README names them per distribution: build-essential, pkg-config, libgtk-3-dev, libwebkit2gtk-4.1-dev and libayatana-appindicator3-dev on Debian and Ubuntu, with the equivalents on openSUSE. The setup script downloads a prebuilt tjs from the releases, or builds txiki.js from source with TJS_BUILD=1, which needs cmake and ninja.

Windows has its own script, and it is a PowerShell one-liner:

```powershell
irm https://tinyjs.app/install.ps1 | iex
```

It installs prebuilt binaries to %LOCALAPPDATA%\tinyjs, overridable with TINYJS_HOME, pins with TINYJS_VERSION, and adds the directory to the user PATH, so a new terminal is needed afterwards. The only runtime requirement is the WebView2 runtime, which the README notes is preinstalled on Windows 11.

One caveat applies to both Linux and Windows: prebuilt binaries only exist for releases from the point where support for that platform merged, and the installer says so plainly when you try an older release.

## tinyjs new and tinyjs dev: your first application

Creating a project is three commands:

```sh
tinyjs new myapp
cd myapp
tinyjs dev
```

A window opens. The dev loop is where the design pays off, and the README describes the two halves precisely. Edit anything in src/frontend/ and the page re-renders in place with caches bypassed: no restart, and the backend state survives. Edit the backend sources, meaning the backend entry's folder, and the process restarts automatically.

That split is the correct behaviour for the two halves of an app, and it is a consequence of the architecture rather than a feature bolted on. The frontend is a page reloaded in a webview, so replacing its content is cheap. The backend is a process with system access, so changing it means restarting it.

The repository carries a test/ directory and a skill/ directory, which suggests the project ships both a test harness and a written skill for working with it, and the several TODO-*.md files at the root, covering audio filters, audio sampler, Linux, PDF, site wrapper, txiki, verify and Windows, are a visible map of the work in flight.

The frontend is plain web technology, and the README is clear that this is a feature and a constraint at once. The backend is plain JavaScript with full system access, so there is no need for a native addon or a bridge, but there is also no abstraction between your code and the operating system.

## The Linux media story, where the system webview shows its seams

The longest part of the README concerns media playback on Linux, and it is the most useful part for anyone shipping a real application. The problem is not the toolchain, it is WebKitGTK, and the README treats it as a documented constraint rather than a bug.

WebKitGTK decodes through GStreamer, and a stock desktop install carries only part of the plugin set. With the base set, an audio or video element plays MP3, Ogg/Vorbis, Opus, WAV and FLAC. AAC and M4A will not play at all, and the README shows how you can check that yourself: canPlayType('audio/mp4; codecs="mp4a.40.2"') returns an empty string. Media Source Extensions report every type as unsupported, which takes out most podcasts, most internet radio and most adaptive streaming. The missing pieces are the GStreamer bad, ugly and libav plugin packages, and WebKit itself hints at the problem at startup, logging that it was not able to find a WebVTT encoder unless gst-plugins-bad is installed.

The second Linux finding is sharper. The README says not to play audio through Web Audio on Linux, and explains why: WebKitGTK renders the Web Audio graph on a normal-priority SCHED_OTHER thread while its media threads get real-time priority, so anything reaching the destination misses its deadline and crunches, on an idle machine, at any latencyHint. There is no graph-side fix, buffering only softens it, and rtkit will not promote the thread.

The recommended path on Linux is to play the element directly and take analysis from the app's own tap:

```js
if (tiny.system.isLinux()) {
  audio.volume = volume;
  await tiny.audioTap.start({ scope: 'app' });
} else {
  const src = ctx.createMediaElementSource(audio);
  src.connect(gain); gain.connect(ctx.destination);
}
```

The scope: 'app' option captures only your own output through a private PipeWire null sink, so playback is unaffected and the rest of the desktop is not recorded. What you give up is anything that was a graph node, such as an equalizer or a StereoPanner, and the README's advice is to tell the user rather than leaving dead controls on screen. For sound effects, tiny.audio.sampler mixes decoded samples with per-voice volume, pan and pitch natively in the launcher on Linux and through Web Audio elsewhere, with the same API. The examples/amp directory holds the full pattern.

An analysis-only graph is a third case that the README separates out: a MediaElementSource feeding nothing but an AnalyserNode never reaches the destination, so a missed deadline costs a dropped frame rather than audible crackle.

## Desktop integration, packaging and updates

The native integration is where a webview-based app has to earn its keep, and the README lists what is covered: native menus, dialogs, tray and menu-bar apps, notifications, drag and drop with real file paths, and window control.

Drag and drop with real paths deserves a note. In a browser, a dropped file is a name and a sandboxed handle. On the desktop it can be a path, and an app that receives one can act on the actual file rather than on a copy.

The packaging story is short. Hot reload in dev, signed .app bundles out of build, auto-update out of publish. The repository layout supports that division: a template/ directory holds the project template, native/ the native launcher sources, runtime/ the runtime pieces, cli.js the command line, scripts/ the packaging scripts, and setup.sh and setup.ps1 the source installers for Unix and Windows.

The release system is also visible in the tree. CHANGELOG.md sits at the root, and the README links to the release history at tinyjs.app/changelog, alongside the full documentation at tinyjs.app/docs.

The rate of change is the other side of this. The recent releases are v0.57.1, v0.57.2 and v0.57.3, all published on 2026-10-15, and the last push was on 2026-10-15. A toolchain that moves several times in a day will have a moving target in its documentation, so pinning a version with TINYJS_VERSION is the sensible habit for anything you depend on in CI.

## Where tinyjs is the wrong tool

The first case is anything that needs a uniform web platform. Using the system webview means macOS gets WebKit, Linux gets WebKitGTK and Windows gets WebView2, and those are not the same engine. The README's Linux media section is a catalogue of what that costs. If your product has to behave identically on three operating systems, a bundled runtime, however large, removes a whole category of bug reports, and tinyjs is deliberately the opposite choice.

The second case is media applications on Linux, for the reasons above. The AAC and M4A gap alone rules out a large part of the podcast and radio world, and while the GStreamer plugin packages close it, a user on a locked-down or minimal system may not be able to install them.

The third case is a team that wants a framework. tinyjs gives you a project skeleton, a dev loop, packaging and updates. It does not give you routing, state management, a component library, a test runner for the frontend, or a form library. The template/ directory and the skill/ directory suggest the project expects you to make those choices, and for a single developer with one small app that is a good trade.

The fourth case is a project that values long-term predictability over small binaries. The version numbers in the 0.57 range and the release cadence say this is a fast-moving tool, and a fast-moving tool is a poor foundation for a product with a decade-long support commitment.

The alternative to tinyjs is the obvious one, and the difference is the point. A Tauri-style application takes the same approach, using a system webview for the frontend and a separate runtime for the backend, and it exists in a more mature state with a larger plugin ecosystem and a steadier release cadence. The difference in approach is where the backend language lives and how large the shipped artefact is, and for a team that values maturity over a smaller download, that trade can favour the more established option. Node and Deno remain the other two answers, and they differ in kind: they ship a runtime rather than a system webview, so the size argument disappears and the platform-consistency argument returns.

## Conclusion

tinyjs fits a developer who wants a small native desktop app, is comfortable writing plain frontend code, and would rather ship a few megabytes than a bundled browser runtime. It does not fit a team that needs a uniform web platform across operating systems, a media application, or a product that expects the long-term stability of a large project. Verify first that your app does not depend on codecs or Web Audio graphs that WebKitGTK lacks, install the GStreamer plugin packages on Linux, and read the platform notes in the README before promising a feature list, because the Linux caveats are documented in detail and the Windows and macOS paths are not equally constrained.

## FAQ

### How do I install tinyjs?

On macOS and Linux run curl -fsSL https://tinyjs.app/install | sh, which detects the operating system, installs to ~/.tinyjs and symlinks tinyjs onto your PATH. On Windows use irm https://tinyjs.app/install.ps1 | iex, which installs to %LOCALAPPDATA%\tinyjs. Pin a version with TINYJS_VERSION and update later with tinyjs update.

### What does tinyjs need on Linux?

The system WebKitGTK 4.1 runtime is required, installed as libwebkit2gtk-4.1-0 on Debian and Ubuntu or libwebkit2gtk-4_1-0 on openSUSE. For media playback you also need the GStreamer bad, ugly and libav plugin packages, otherwise AAC and M4A will not play and Media Source Extensions report every type as unsupported.

### How do I create and run a tinyjs application?

Run tinyjs new myapp, then cd myapp and tinyjs dev, which opens a window. The dev command hot-reloads the frontend when you edit files in src/frontend/ and restarts the backend process when you edit the backend entry's folder, so backend state survives a frontend edit.

### How does the frontend talk to the backend in tinyjs?

There is no HTTP server and no ports. The README states that the page and the backend talk over a Unix domain socket in a private temp directory, with the backend running as plain JavaScript on txiki.js with full system access for files, sockets, processes and FFI.

### What licence is tinyjs released under and how often does it change?

The project is licensed under MIT, with a LICENSE file in the repository. The recent releases v0.57.1, v0.57.2 and v0.57.3 were all published on 2026-10-15, and the last push was on 2026-10-15, so pinning a version is the sensible habit for CI use.

## Sources

- [Issues](https://github.com/tarwin/tinyjsapp/issues)
- [License: MIT](https://github.com/tarwin/tinyjsapp/blob/main/LICENSE)
- [README](https://github.com/tarwin/tinyjsapp/blob/main/README.md)
- [Releases](https://github.com/tarwin/tinyjsapp/releases)
- [tarwin/tinyjsapp on GitHub](https://github.com/tarwin/tinyjsapp)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tarwin-tinyjsapp
