Download

Publish

Publish an extension store

An extension store is one JSON file, and the APKs and icons it points to, on any web server. Users add its address once and get every update from it.

Sign every release with one key #

Make a key once, and keep it somewhere safe:

Shell
keytool -genkeypair -keystore extensions.jks -alias extensions -keyalg RSA -keysize 4096 -validity 36500

Then build each release, align it, sign it, and read the key's fingerprint:

Shell
./gradlew assembleRelease
zipalign -f 4 build/outputs/apk/release/example-comics-release-unsigned.apk example-comics.apk
apksigner sign --ks extensions.jks --ks-key-alias extensions --v4-signing-enabled false example-comics.apk
apksigner verify --print-certs example-comics.apk

zipalign and apksigner are in the Android SDK's build-tools. Of what the last command prints, the store needs the SHA-256 digest, in the same form: 64 lowercase hexadecimal digits without colons. keytool prints it in capitals with colons, which matches nothing.

Signer #1 certificate SHA-256 digest: 0ea9de3d5a682608a15b101000c032d5aaae7e12902fc6bf8bb551de1aa00124

Katari installs an extension from a store only if the APK is signed with the store's key, and trusts it without asking for the same reason.

The index #

The index describes the store and lists its extensions:

index.json
{
  "name": "Example Extensions",
  "badgeLabel": "Example",
  "signingKey": "0ea9de3d5a682608a15b101000c032d5aaae7e12902fc6bf8bb551de1aa00124",
  "contact": { "website": "https://example.org" },
  "extensionList": {
    "extensions": [
      {
        "name": "Example Comics",
        "packageName": "org.example.katari.examplecomics",
        "resources": {
          "apkUrl": "https://example.org/katari/apk/example-comics.apk",
          "iconUrl": "https://example.org/katari/icon/example-comics.png"
        },
        "extensionLib": "4.0",
        "versionCode": 1,
        "versionName": "4.0.0",
        "contentWarning": "SAFE",
        "sources": [
          {
            "id": 4316702938517726312,
            "name": "Example Comics",
            "language": "en",
            "homeUrl": "https://comics.example",
            "supportedEntryTypes": ["MANGA"]
          }
        ]
      }
    ]
  }
}
Field What it holds
name, badgeLabel Required. The store's name, and the short label shown on each of its extensions.
signingKey Required. The SHA-256 digest of the signing certificate, as apksigner prints it.
contact.website, contact.discord Where users find out about the store. The website is required, the Discord invite optional.
extensionList.extensions The extensions. Leave extensionList out and give extensionListUrl instead to keep the list in a file of its own.

Every field of an extension is required:

Field What it holds
name, packageName The extension's name and its applicationId.
resources.apkUrl, resources.iconUrl Full addresses of the APK and its icon, which is a square PNG.
extensionLib The SDK the extension is built with, the first two parts of its versionName, such as "4.0". Katari hides extensions built with an SDK it doesn't load, and a value that isn't numbers, such as "3.x", fails the whole store's list.
versionCode, versionName The same as in the APK. Katari offers an update when versionCode grows, or the SDK in versionName is newer.
contentWarning SAFE, MIXED or NSFW, matching the manifest.
sources Each source's id, name and language, required, and homeUrl, optional, as the source itself has them. List at least one. Katari lists each source on its own before the extension is installed, in the languages the user turned on, and uses them to name a source a backup refers to and offer its extension.
supportedEntryTypes Optional, on each source: MANGA, ANIME or BOOK, for the type badges and the Content types filter. Leave it out and the source shows only when that filter includes unspecified sources.

The index and a separate extension list can be served gzipped, and as Protobuf instead of JSON. Katari reads Tachiyomi's and Aniyomi's stores as well, but new stores should use this format.

Put it online #

  1. Upload index.json, the APKs and the icons to any server that serves files over HTTPS, such as GitHub Pages.
  2. Tell users the address of index.json. They paste it under Browse › Extensions › ⋮ › Extension stores, as in Extension stores.
  3. For a button on a web page that adds the store, link to katari://extension-store?url= followed by the index's address, encoded as a URL parameter. Katari opens and asks the user to confirm.

To release an update, upload the new APK, raise its versionCode and versionName in the index, and Katari offers it the next time it checks the store. An installed extension the index no longer lists keeps working, marked Orphaned.