Building sources

Publishing a source repository

Host your own Boundless extensions with a versioning.json manifest and static files served over HTTPS.

5 min read Updated 22 August 2026

A Boundless repository is not a service. It is a directory of static files served over HTTPS: one JSON manifest and one JavaScript file per source. There is no server code, no build step and no registration process. Anyone who can host a folder can host a repository, and users install from it by pasting the URL into the app under Settings, Sources, Add from Repo URL.

Layout

your-repo/
├── versioning.json
├── gutenberg/
│   └── index.js
└── another-source/
    └── index.js

The app fetches <repo-url>/versioning.json, then resolves each source's code as <repo-root>/<id>/index.js. The folder name must equal the source id in the manifest exactly, or the install fails with a 404.

If the URL a user pastes already ends in .json, the app uses it directly as the manifest and resolves bundles relative to that file's directory. That means you can serve a manifest under any name or path you like, as long as the per-source folders sit beside it.

versioning.json

A complete manifest with two sources:

{
  "repositoryName": "Example Sources",
  "repositoryDescription": "Public-domain and openly licensed catalogues.",
  "buildTime": "2026-08-22T00:00:00Z",
  "sources": [
    {
      "id": "gutenberg",
      "name": "Project Gutenberg",
      "version": "1.0.0",
      "medium": "novel",
      "website": "https://www.gutenberg.org",
      "description": "70,000+ public-domain books from Project Gutenberg's catalog API. Browse, search, genre filtering and full in-app reading.",
      "capabilities": ["browse", "search", "genres", "details", "chapters", "reader"],
      "language": "en"
    },
    {
      "id": "openlibrary-comics",
      "name": "Example Comics",
      "version": "0.3.1",
      "medium": "comics",
      "website": "https://example.com",
      "description": "An openly licensed comics catalogue.",
      "capabilities": ["browse", "search", "details", "chapters", "reader"],
      "language": "en"
    }
  ]
}

Top-level fields

Field Type Notes
repositoryName string Display name for the repository.
repositoryDescription string One line describing what the repository carries.
buildTime string ISO 8601 timestamp of the last publish. Useful for cache busting and for humans.
sources array Required. The source listing.

Source entries

Field Type Notes
id string Required. Also the folder name. This is the source's identity, see versioning below.
name string Required. Shown in the install list and as the source name in the app.
version string Defaults to 1.0.0 if omitted. Drives the update prompt.
medium string "novel" or "comics".
website string The source site, for listing pages.
description string What the source offers, in one or two sentences.
capabilities array of strings Conventional values: browse, search, genres, details, chapters, reader.
language string ISO language code of the catalogue, such as en.

The app itself reads only id, name and version when listing a repository. Everything else is metadata for humans and for repository landing pages, so keep it accurate but do not expect the app to enforce it.

Versioning and updates

The app compares the version string in your manifest against the version it recorded when the user installed the source. If the strings differ, the entry shows an Update button. If they match, it shows Reinstall. The comparison is plain string equality, not semantic version ordering, so any change to the string counts as an update, including a downgrade.

Two rules follow from that:

  1. Bump version in the same commit that changes index.js. A code change published under the same version reaches nobody who already installed the source, because the app never offers them the update.
  2. Never reuse an id for a different source. The id is the identity key: installing an entry overwrites any installed extension with the same id, keeping the id and replacing name, version and code. Renaming a source is fine. Repurposing an id is how a user ends up with one source's name on another source's code.

The app has no background update check. Users see updates when they open the repository screen, so a changelog on your listing page is worth keeping.

Hosting

Any static host works: Firebase Hosting, GitHub Pages, Netlify, Cloudflare Pages, an S3 bucket behind CloudFront, or a directory on a VPS with nginx in front of it. The only requirements are that the files are reachable over HTTPS and served with the right content type.

HTTPS is not optional. iOS blocks plaintext HTTP by default, so a repository served over http:// will fail to load in the app even though it opens fine in a desktop browser.

A GitHub Pages repository needs an empty .nojekyll file at the root, otherwise directories are processed in ways that can break paths. If you serve index.js files from a host that guesses content types, confirm they come back as application/javascript or text/javascript.

Test the two URLs by hand before telling anyone the repository is live:

curl -sI https://your-repo.example/versioning.json
curl -s  https://your-repo.example/gutenberg/index.js | head -5

Anything in the served directory is public. This is the practical trap. A repository is a web root, so scratch files, .env files, API keys in a config, notes to yourself, draft extensions for sources you have not decided to publish and anything else sitting in that folder are all fetchable by anyone who guesses or crawls the path. Keep the served directory limited to the manifest, the source folders and your listing page. Put working files somewhere the deploy does not reach, and check your host's ignore configuration rather than assuming a leading dot hides a file.

Responsible sourcing

An extension is a client that reads pages on a user's own device, but what you point it at is a choice, and it is yours.

Prefer public-domain and openly licensed catalogues. Project Gutenberg, Standard Ebooks, Wikisource, Internet Archive collections with clear rights statements, and publishers who distribute their own work openly all offer full catalogues you can index without ambiguity. Where a catalogue exposes a rights field, filter on it rather than assuming: the Gutenberg source in the official repository drops any record whose copyright field is not explicitly false, so nothing with an unclear status can surface.

Respect the source site. You are writing code that other people will run against someone else's infrastructure, at whatever scale your source gets installed.

  • Do not hammer it. Prefer sequential requests to a wide Promise.all fan-out, and never poll.
  • Ask for the smallest thing that answers the question. Fetch a listing endpoint rather than N detail pages when the listing already carries the fields you need.
  • Let the platform cache. The app's networking layer has an HTTP cache, so honour cache headers instead of adding cache-busting query parameters.
  • Send an honest, stable User-Agent, and follow the site's stated terms and robots.txt position.
  • If a site publishes an API, use it rather than scraping the pages around it.

The official Boundless repository lists licensed and public-domain content only. That is the standard applied there, and the reasoning generalises: the more defensible your catalogue's rights position, the less fragile your repository is.

Before you publish

  • [ ] Every behavioural assertion in Testing extensions passes, twice in a row.
  • [ ] No fetch, URL, setTimeout, require or Node built-in appears anywhere in the file. See the sandbox rules.
  • [ ] Every field name matches the API reference, including image for covers and epoch milliseconds for chapter times.
  • [ ] The folder name equals the manifest id.
  • [ ] version was bumped and buildTime updated.
  • [ ] versioning.json and <id>/index.js both load over HTTPS from the URL you are about to publish.
  • [ ] The served directory contains nothing you would not put on a public web page.