Boundless Reader ships with no content sources built in. A fresh install has an empty Library, an empty Browse tab, and nothing to search. Everything those screens show comes from a source: one JavaScript file that knows how to talk to a single site. You install sources from a repository URL, and the file runs on your own device, never on a Boundless server. You need JavaScript and either the site's HTML or its API docs. Nothing else.
This is a tour. The contract itself lives in the docs: the overview, the API reference, testing and publishing. Read them before you ship anything. I am pointing at those pages rather than retyping them, because the field names are the part that breaks things, and a summary is the wrong place to get one wrong.
Only write a source for a site you run, a site whose operator has given you permission, or public-domain and openly licensed material. Do not build one that gets around a login, a paywall or any other access control. The overview and publishing pages spell out what you are responsible for as an author and as a publisher.
The shape of a source
A source is a single index.js that defines a class called Source and exports it:
class Source {
async getSearchResults(request, metadata) { /* ... */ }
async getMangaDetails(mangaId) { /* ... */ }
async getChapters(mangaId) { /* ... */ }
async getChapterDetails(mangaId, chapterId) { /* ... */ }
}
module.exports = { Source };
Those four methods are required. Two more are optional: getSourceFeeds for the tabs on the Browse screen, and getSearchTags for the genre picker. That is the whole surface. Search results, manga details, a chapter list, and one chapter's pages or text.
Your file runs in a sandboxed JavaScript runtime on the device, with no bundler and no npm install. There is no fetch, no URL, no setTimeout, no DOM, and none of Node's built-ins. Networking goes through App.createRequestManager().schedule(request) instead, which the host performs natively and hands back as { status, headers, data }, with data always a plain string you parse yourself. Reach for a missing global and nothing stops you at install time. It throws the moment that line runs, and the symptom on a real device is a blank Browse tab with no error anywhere.
Testing it before you trust it
A 200 response from a site proves almost nothing. The testing doc lists real extensions that returned 200 on every call while quietly doing the wrong thing: a page parameter the API accepted and ignored, a chapter list capped at whatever the default page size happened to be, a has_more flag that stayed true past the actual last page, and four Browse feeds that passed different sort values and got identical results back. None of that throws an exception. It only shows up once you check the content instead of the status code.
So check the content. A few of the assertions that matter most:
- Fetch page one and page two of a feed and confirm the ids do not overlap.
- Walk pages until the source stops handing back another one, with a cap on the loop, so a broken source fails loudly instead of running forever.
- Confirm every cover URL is absolute.
- Confirm chapter times are numbers in epoch milliseconds. A
Dateobject or an ISO string is silently discarded. - Run the whole check again afterward. A source that fires off twenty requests at once often passes cold and fails warm, and a reader browsing for two minutes is the warm case.
The testing doc includes a short Node harness that stubs the two host globals your code touches, which is enough for a JSON API. It is worth knowing where that harness stops being faithful, too. Node carries far more globals than the app's sandbox does, so code that runs cleanly there can still fail on a device the moment it hits one the sandbox does not have. URLSearchParams is the one exception, since Boundless installs a shim for it before your script runs. Nothing else in that family gets one.
Publishing it
A repository is a folder served over HTTPS, and nothing more: one versioning.json manifest, and one folder per source holding an index.js. No server code, no build step, no registration. The folder name has to match the source's id in the manifest exactly, or installing it fails with a 404.
Two rules matter more than the rest of the checklist. Bump the version string in the same commit that changes the code, because the app only offers an update when that string changes, and it compares it as plain text rather than as a version number. And never reuse an id for a different source. Installing an entry overwrites whatever already carries that id, so repurposing one silently replaces the source your existing users installed with a different one.
HTTPS is not negotiable either. iOS blocks plaintext HTTP by default, so a repository served over http:// opens fine in a desktop browser and fails to load in the app.
What you point it at
An extension runs on someone else's device, but which pages it reads there is a choice, and it is yours. The official Boundless repository, at https://boundless.moe/repo, sticks to public-domain and openly licensed catalogues on purpose, so that installing from it raises no questions worth asking twice. Your own repository does not have to match that choice, but the same questions still apply to it: whose infrastructure your code is hitting, at whatever scale your source gets installed, and whether the catalogue's rights position holds up if someone asks about it. Write sources for sites you run or have permission to use, or for public-domain and openly licensed material, and leave out anything that works around a site's access controls.
If you are ready to start, the overview has the full sandbox rules and a complete working example against a public API, and the API reference has the field names worth keeping open in a second tab while you write the mapping code.