# BangleApps: a browser-based app loader for the Bangle.js smartwatch

> BangleApps is the Bangle.js App Loader and its app catalogue, delivered to the watch over Web Bluetooth from a web page that reads apps.json and compares it with the files on the device. The release and development sites differ only in how often they are refreshed.

**espruino/BangleApps** — Bangle.js App Loader (and Apps)

- Repository: https://github.com/espruino/BangleApps
- Website: https://banglejs.com/apps
- Stars: 581 · Forks: 1,351
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/espruino-bangleapps

## A web page that installs apps onto a watch over Bluetooth

Bangle.js is an Espruino-based smartwatch, and this repository is the App Loader plus the apps it loads. The homepage is https://banglejs.com/apps, and there are two versions of the whole thing.

The release version is manually refreshed with regular intervals, while the development version is continuously updated as new code is committed to the repository. That distinction is the first thing to understand, because it determines where you send a user.

A user of the release site gets a curated snapshot of the catalogue. A user of the development site gets whatever was committed most recently, which is where new work appears first and where breakage appears first too. The development site is at espruino.github.io.

The primary language is JavaScript, the licence is MIT, and the default branch is master. The repository has no GitHub releases, which is consistent with the fact that the release artefact is a website rather than a package. The last push was on 2026-10-22, and the topics list hacktoberfest, javascript, smartwatch and webbluetooth, which names the transport that makes the whole thing work.

## apps.json, and the three-step upload it drives

The How does it work section describes the mechanism in five steps, and they are worth following because the design explains most of the rest of the repository.

A list of apps is in apps.json, and that file is auto-generated from all the apps/yourapp/metadata.json using Jekyll or bin/create_apps_json.sh. Each element references an app in apps/<id> which is uploaded.

When it starts, BangleAppLoader checks the JSON and compares it with the files it sees in the watch's storage. To upload an app, BangleAppLoader checks the files that are listed in apps.json, loads them, and sends them over Web Bluetooth.

So the loader is a diffing installer. It fetches a manifest, asks the watch what it already has, and sends only what is missing. That is why apps.json is generated rather than hand-maintained: it is a build artefact, and the generation script is checked into the repository, and the npm script update-local-apps runs ./bin/create_apps_json.sh against a separate apps.local.json for local work.

The two entry points in the tree make the same point. index.html is the loader page itself, loader.js is the logic that compares and uploads, and install_from_files.js is the path that bypasses the loader entirely.

## The 28 character limit shapes every filename

Filenames in storage are limited to 28 characters, and the whole naming scheme exists to work around that.

Each file type gets a suffix, so you can tell what a file is at a glance. appid.info is JSON that describes an app and is auto-generated by the App Loader. appid.img is an image. appid.app.js is JS code for applications. appid.wid.js is JS code for widgets, and appid.clkinfo.js is JS code for a clock info. appid.settings.js is JS code for the settings menu, and appid.boot.js is JS code that automatically gets run at boot time. appid.json is used for JSON settings for an app.

The 28 character limit is the reason the App Loader has to generate appid.info itself rather than accepting one from the app author, since there is no room left to spare for an app to also carry its own description file.

The limit also shapes the app id, and the documentation is explicit about it. Come up with a unique name, all lowercase, no spaces, and we will assume myappid. Bangle.js is limited to 28 char filenames and appends a file extension such as .js, so please try and keep filenames short to avoid overflowing the buffer. The examples section repeats the advice with a number: the max filename length is 28 chars, so we suggest an app ID of under 20 so that when .app.js and so on gets added to the end the filename isn't cropped.

A cropped filename is a subtle failure, because the app still loads and the file simply has a different name than the one in the manifest.

## The metadata.json that defines an app

Adding an app to the menu starts with a folder and a metadata file. Create a folder called apps/<id>, assume apps/myappid, and create or modify apps/myappid/metadata.json.

```json
{ "id": "myappid",
  "name": "My app's human readable name",
  "shortName" : "Short Name",
  "icon": "app.png",
  "description": "A detailed description of my great app",
  "tags": "",
  "storage": [
    {"name":"myappid.app.js","url":"app.js"},
    {"name":"myappid.img","url":"app-icon.js","evaluate":true}
  ],
},
```

The storage array is the interesting part. Each entry maps a name in the watch's storage to a file in your app folder, and the evaluate flag on the icon entry means that file is evaluated rather than stored as plain text, which is how a generated image string becomes a drawable image.

The icon itself has its own rule. apps/myappid/app.png should be a 48px icon, and the documentation points at the Espruino Image Converter to create apps/myappid/app-icon.js, using a 1 bit, 4 bit or 8 bit Web Palette Image String. That conversion step is not optional.

Screenshots are optional and additive. In the app metadata.json file you can add a list of screenshots with a line like a screenshots array of objects with a url. Two ways of getting them are given: typing g.dump() in the left-hand side of the Web IDE when connected to a Bangle.js 2, which only works on Bangle.js 2 because Bangle.js 1 is unable to read data back from the LCD controller, or running your code in the emulator and using the screenshot button in the bottom right of the window.

## Two ways to test, online and offline

The Testing section gives two routes, and the choice between them is about who does the work.

The online route is described as the best way to test. You fork the repository, add your files, go to GitHub Settings and activate GitHub Pages, and run your personal Bangle App Loader at the github.io address for your fork to load apps onto your device. If your apps are not appearing, the instruction is to check your web browser's developer console for errors.

The offline route uses the Storage icon in the Web IDE. With four discs available, you upload your files into the places described in your JSON, uploading app-icon.js as myappid.img. Then you load app.js in the editor and click the down-arrow at the bottom right of the Send to Espruino icon, choose Storage, then either choose myappid.app.js if you uploaded your app previously, or New File and enter myappid.app.js as the name.

Clicking the Send to Espruino icon then loads the app directly into Espruino and automatically runs it. The documentation adds a detail that is easy to miss: when you upload code this way, your app will even be uploaded to Bangle.js's menu without you having to use the Bangle App Loader.

There is a catch for widgets. Widgets need to be run inside a clock or app, so if you are developing a widget you need to go to Settings, then Communications, then Load after saving, and set it to Load default application.

The local sanity check is also part of the workflow. It is a great idea to get a local copy of the repository on your PC, then run bin/sanitycheck.js, which runs through a bunch of common issues. To get the project running locally you have to initialize and update the git submodules first with git submodule update --init.

## The npm scripts, and what the test command actually runs

The package.json is small and its scripts describe the project's whole contributor workflow.

The test command is the interesting one, because it is three things chained together: node bin/sanitycheck.js, then the lint-apps script, then lint-modules.

```json
    "test": "node bin/sanitycheck.js && npm run lint-apps && npm run lint-modules",
```

The lint-apps script first runs node bin/sync-lint-exemptions.mjs, which regenerates a list of files exempt from linting, and then runs eslint with --max-warnings 0 over the apps directory. The zero tolerance on warnings means the apps tree is held to a stricter standard than the rest of a typical JavaScript project, which makes sense when the code is running on a device with a small memory budget and no debugger.

The lint-modules script runs eslint the same way over the modules directory. The fix script applies eslint --fix to both directories.

For local development there are two more scripts worth knowing. The local script runs npm-watch and npx http-server together, so the apps.json is regenerated as metadata files change and served locally with caching disabled, and the start script serves the same way without the watcher. The watch configuration ties the update-local-apps script to the glob apps/*/metadata.json, so a metadata edit triggers a regeneration of the local manifest.

The devDependencies are the usual JavaScript ones: eslint with the airbnb-base config and the import plugin, the typescript-eslint parser and plugin, http-server for serving, and npm-watch. The only runtime dependency is acorn, a JavaScript parser, which is the piece that lets the tooling inspect the app source rather than only diffing file names.

## Where this approach stops working

There are three limits, and the first is structural.

The loader needs a web browser with Web Bluetooth, which means a Chromium-based browser on a machine with Bluetooth, and it means the watch has to be in range. A field technician provisioning a device in a server room without a usable browser has no path. The offline route in the Web IDE exists for that case, and it is more manual: you upload files through the IDE storage dialog and push them over the serial or debug connection instead.

The second limit is the file size budget. Twenty-eight characters of filename, and a watch with limited storage, means an app has to be small and its identifiers short. That constraint is not incidental to the design, it is the design, and it is why the documentation keeps telling you to keep the app id under 20 characters and the filename short.

The third is the two-site split. The release site is manually refreshed, so a newly merged app sits on the development site until a maintainer refreshes the release. If you depend on the release site for distribution, your app has a delay before users see it that you do not control. And the development site is updated on every commit, so an unreviewed change to the loader itself can affect anyone using it.

The realistic alternative is not another loader but a different delivery model: ship the app to users as part of a firmware build, or send it over the debug cable with a script. That skips the web page and the manifest entirely, at the cost of the distribution convenience that the loader exists to provide. For a hobbyist publishing a watch face, the loader is the right shape. For an organisation that needs a controlled, versioned firmware distribution with an audit trail, it is not, and the version-control-of-firmware route is the better fit.

## Conclusion

Adopt BangleApps if you build firmware for Bangle.js hardware and want your apps to reach users over Web Bluetooth without a cable or a local install, since that is exactly what the loader does. Do not adopt it as an offline deployment method, because the loader is a web page. Before you publish, note the 28 character filename limit that drives the whole file naming scheme, and that the last push was on 2026-10-22 with the release site refreshed manually while the development site updates on every commit.

## FAQ

### How does the Bangle.js App Loader get apps onto the watch?

The loader reads the apps.json manifest, compares it with the files it sees in the watch's storage, and sends the missing files over Web Bluetooth. The manifest is auto-generated from every apps/yourapp/metadata.json using Jekyll or bin/create_apps_json.sh.

### What is the difference between the release and development versions of BangleApps?

The release version is manually refreshed with regular intervals, while the development version is continuously updated as new code is committed. The release site is at banglejs.com/apps and the development site at espruino.github.io.

### Why does Bangle.js limit app filenames to 28 characters?

Filenames in storage are limited to 28 characters, which is why the file naming scheme uses a short app id plus a type suffix such as app.js, wid.js, clkinfo.js, settings.js, boot.js or img. The documentation suggests an app ID under 20 characters so the extension does not push the name over the limit and get it cropped.

### How do I test a Bangle app offline without the App Loader?

Use the Storage icon in the Web IDE, uploading app-icon.js as myappid.img, then load app.js in the editor and use the down-arrow on the Send to Espruino icon to write myappid.app.js to storage. Sending that way also adds the app to the watch's menu without the Bangle App Loader.

### What does the npm test command for BangleApps run?

It runs node bin/sanitycheck.js, then the lint-apps script, which syncs the lint exemptions and runs eslint with --max-warnings 0 over the apps directory, then lint-modules over the modules directory.

## Sources

- [espruino/BangleApps on GitHub](https://github.com/espruino/BangleApps)
- [Issues](https://github.com/espruino/BangleApps/issues)
- [License: MIT](https://github.com/espruino/BangleApps/blob/master/LICENSE)
- [Project website](https://banglejs.com/apps)
- [README](https://github.com/espruino/BangleApps/blob/master/README.md)

---

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