Contribute
Publish a workout pack.
Anyone can run a community repository: a coach with a structured plan, a training group sharing their winter intervals, or a single rider who's built a pack worth handing out. The process is two static JSON files and a pull request.
The shape of the deal
-
You write a
manifest.jsondescribing your repository (name, author, homepage) with all your workouts embedded inline. - You host it over HTTPS, anywhere that serves a JSON file with permissive CORS. GitHub Pages, Codeberg Pages, your own web folder.
-
You open a pull request against this app's
Codeberg repository
adding a single entry to
website/registry.jsonthat points at your manifest URL. - On merge, your repository shows up in the community browser and is subscribable from inside the app.
1. Write your manifest (and bundles)
A repository is two kinds of file:
-
One small
manifest.jsonwith repo metadata plus a list of pointers to your bundle files. The app downloads this on every refresh, so keeping it small (just metadata, no segments) keeps everyone's bandwidth low. - One or more bundle files, each a JSON array of workout objects. The app downloads a bundle only when its content version changes, so unrelated workouts stay cached when you edit one.
Manifest top-level fields
schemaVersionintegeryes
Manifest format version. Currently 2. The app rejects manifests with versions higher than it understands and shows a "please upgrade" hint.
idstringyes
Stable, unique identifier (kebab-case). Used as a folder name and as the dedup key, so don't change it after publishing.
namestringyes
What's shown on the card. Up to about 40 characters reads well.
descriptionstringno
One or two sentences. Plain text, no markdown.
authorstringno
Your name, your collective, or your handle.
homepagestring (URL)no
Optional link from the repo card to your site.
updatedEpochinteger (ms)no
Unix epoch in milliseconds. Lets the app show "updated 3 days ago".
bundlesarrayyes
Pointers to your bundle files. See the next table. Every published workout lives in some bundle; the manifest itself never contains segments.
Each bundle entry
idstringyes
Stable bundle id (kebab-case). Used as the cache key on the app side, so don't rename after publication.
namestringno
Maintainer-facing label. Bundles aren't shown to end users; this only helps you and other contributors.
schemaVersionintegeryes
Format version of the bundle file. Currently 1 = bare JSON array of workout objects. Bumped if/when the bundle wrapper changes.
versionintegeryes
Bump this every time you change any workout in the bundle, even by one second. The app re-downloads only when this number increases. Forgetting to bump it is the #1 cause of "my edit didn't show up".
urlstringyes
Absolute (https://…) or relative to the manifest's URL. Relative is the portable form: survives a host change.
Each workout (inside a bundle file)
A bundle file is a JSON array; each element is a workout object:
schemaVersionintegerno
Workout format version. Defaults to 1. Workouts with versions higher than the app understands are skipped, so future formats can land in an existing bundle without breaking older clients.
idstringyes
Unique within your repo. Prefix with your repo id to avoid collisions with bundled or other-repo workouts (e.g. "alpine-tempo-3x10" rather than just "tempo-3x10").
namestringyes
Display name. Keep it short; it shows on small cards.
descriptionstringno
Why someone would ride this. One paragraph max.
tagsarray<string>no
Free-form labels: "Threshold", "VO2 Max", "Recovery", etc.
segmentsarrayyes
Sequential intervals. Each has lengthInSeconds (1–14400), powerPercentFTP (0–300), and intervalType: "CONSTANT".
Example: minimum viable repo
Two files. Copy, edit, host.
manifest.json
{
"schemaVersion": 2,
"id": "alpine-coach",
"name": "Alpine Coach",
"description": "Threshold and VO2 work tuned for stage racers.",
"author": "Alpine Coach Collective",
"homepage": "https://alpinecoach.example",
"updatedEpoch": 1746547200000,
"bundles": [
{
"id": "main",
"schemaVersion": 1,
"version": 1,
"url": "main.json"
}
]
}
main.json (sibling of the manifest):
[
{
"schemaVersion": 1,
"id": "alpine-warmup-2min",
"name": "Quick Warmup",
"description": "Two-minute pre-effort opener.",
"tags": ["Warm-up"],
"segments": [
{ "lengthInSeconds": 60, "powerPercentFTP": 50, "intervalType": "CONSTANT" },
{ "lengthInSeconds": 60, "powerPercentFTP": 75, "intervalType": "CONSTANT" }
]
}
]
For a longer reference, see the starter pack manifest and its main bundle: six workouts spanning warm-up through VO2.
Splitting workouts across multiple bundles
For small repos, one bundle is fine. Split when you have logical groups
that change at different rates: e.g. weekly-plan bumped
every Monday and warmups updated rarely. Riders only
re-download the bundle whose version moved.
2. Host it
The app fetches your manifest over plain HTTPS. Anywhere that serves static JSON works. Two things matter:
-
HTTPS only. The app rejects
http://on import (defense against tampering on hostile networks). -
CORS. Your host must send
Access-Control-Allow-Origin: *(or includeindoorbike.app) so the community browser'sfetch()can read your manifest. Without this, your repo will work in the Android app but won't preview on the website. GitHub Pages, Codeberg Pages, and Cloudflare Pages all set this header by default for static files.
Test your URL by opening it directly in a browser. If you see your JSON, you're good. If you see "blocked by CORS" in the devtools console, fix your host's headers.
3. Submit to the registry
Listing your repo in the community browser is a one-line addition to
website/registry.json. Open a pull request on the
app's Codeberg repository:
- Fork the repository.
-
Edit
website/registry.jsonand append a new entry to therepositoriesarray. The fields:
The{ "id": "alpine-coach", "name": "Alpine Coach", "description": "Threshold and VO2 work tuned for stage racers.", "author": "Alpine Coach Collective", "manifestUrl": "https://alpinecoach.example/manifest.json", "tags": ["Threshold", "VO2 Max"], "homepage": "https://alpinecoach.example" }idhere must match theidin your manifest.tagshere are repo-level (shown on the card); they're independent from per-workout tags. - Open a pull request with a short note about who you are and what the pack covers.
On merge, your repo appears in the community list within minutes (just a static-site rebuild). No backend, no account, no review queue beyond a quick sanity check that the manifest URL is reachable and the JSON parses.
Updating after publication
-
Editing a workout: change the bundle file, then
bump that bundle's
versionin the manifest. The app will see the bumped number on next refresh and re-download just that bundle. Other bundles stay cached. -
Adding a workout: drop it into an existing bundle
file, bump that bundle's
version. Or create a new bundle file and add a fresh entry to thebundlesarray. -
Removing a workout: remove it from the bundle file
and bump that bundle's
version. Existing subscribers stop seeing it after the next refresh. Past rides of that workout stay in their ride history. -
Renaming or rebranding the repo: change the
manifest's
name,description, etc., but don't change theid. The id is the identity; changing it makes the app think it's a different repo. -
Bumping
updatedEpoch: optional, but worth setting on each release so users see "updated 2 days ago" rather than a stale timestamp.
Style guidelines (suggestions, not gates)
-
Prefix workout ids with your repo id. Two repos
shipping
"tempo-3x10"would clash inside the app, and the dedup rule (Imported > Subscribed > Bundled, first-wins) may not pick the one your user wanted. -
Keep
powerPercentFTPrealistic. The app accepts 0–300, but anything above ~150 is sprint territory and most trainers will struggle to actually deliver it in ERG. - Open with a warm-up, close with a cool-down. Saves your riders from cold-start TSS spikes.
- Plain-text descriptions. Markdown isn't rendered anywhere; line breaks are.