diff --git a/.changeset/add-obtainium-app-config.md b/.changeset/add-obtainium-app-config.md
new file mode 100644
index 0000000000..40ad03bcf6
--- /dev/null
+++ b/.changeset/add-obtainium-app-config.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Publish an `obtainium.json` app config with every release so Android builds can be installed and auto-updated through Obtainium.
diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml
index 3d976d4623..6d24d08200 100644
--- a/.github/workflows/tauri-build.yml
+++ b/.github/workflows/tauri-build.yml
@@ -408,6 +408,33 @@ jobs:
gh release upload "$TAG" "$f" --clobber
done
+ obtainium:
+ name: Publish Obtainium config
+ needs: [setup-release, android]
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: write
+ env:
+ TAG: ${{ needs.setup-release.outputs.tag }}
+ VERSION: ${{ needs.setup-release.outputs.version }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ needs.setup-release.outputs.ref }}
+ persist-credentials: false
+
+ - name: Generate obtainium.json
+ shell: bash
+ run: node scripts/obtainium-generate.js "$VERSION" "$TAG" obtainium.json
+
+ - name: Attach Obtainium config to release
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: gh release upload "$TAG" obtainium.json --clobber
+
ios:
name: Build iOS (AltStore/SideStore)
needs: setup-release
diff --git a/README.md b/README.md
index 787f27bee6..a839b7e572 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,32 @@ The stable web app is available at [app.sable.moe](https://app.sable.moe/) and t
You can also download our desktop app for Windows and Linux from [releases](https://github.com/SableClient/Sable/releases/latest). Release artifacts include build attestations, and desktop installations update automatically.
+## Android (Obtainium)
+
+Android APKs are published to every release, and [Obtainium](https://obtainium.imranr.dev) keeps them updated straight from GitHub. Each release also ships an `obtainium.json` app config. Use it for the nightly channel, where prereleases and date-based version tracking have to be enabled to follow the rolling `nightly` tag.
+
+### Stable
+
+
+
+
+
+
+
+### Nightly
+
+
+
+
+
+### Setup & install
+
+1. Install [Obtainium](https://github.com/ImranR98/Obtainium/releases/latest).
+2. For stable, tap the button above, or add `https://github.com/SableClient/Sable` by hand under **Add App**.
+3. For nightly, download the `obtainium.json` above and import it with **Import/Export → Import from file**.
+
+Android builds are produced by the `android` job in [`tauri-build.yml`](.github/workflows/tauri-build.yml), and the config by the `obtainium` job in the same workflow.
+
## iOS (AltStore / SideStore)
Sable iOS builds are distributed as unsigned IPAs through [AltStore](https://altstore.io) and [SideStore](https://sidestore.io). Each release publishes both the IPA and an `altstore-source.json` manifest — stable builds to the [latest GitHub release](https://github.com/SableClient/Sable/releases/latest), nightly builds to the [`nightly` GitHub release](https://github.com/SableClient/Sable/releases/tag/nightly).
diff --git a/scripts/obtainium-generate.js b/scripts/obtainium-generate.js
new file mode 100644
index 0000000000..bd7ec703cf
--- /dev/null
+++ b/scripts/obtainium-generate.js
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+//MISE hide=true
+//MISE description="Generate obtainium.json for one-click app import"
+/* oxlint-disable no-console */
+
+import { writeFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import process from 'node:process';
+
+const version = process.argv[2];
+const tag = process.argv[3] || `v${version}`;
+const outputPath = process.argv[4] || 'obtainium.json';
+
+if (!version) {
+ console.error('Usage: node scripts/obtainium-generate.js [tag] [output-path]');
+ process.exit(1);
+}
+
+const GITHUB_REPO = 'SableClient/Sable';
+const APK_NAME = `Sable-${version}-android-universal.apk`;
+const APK_URL = `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${APK_NAME}`;
+const isNightly = tag === 'nightly';
+
+// Obtainium fills in the rest from its own defaults on import.
+const additionalSettings = {
+ about: 'An almost stable Matrix client',
+ ...(isNightly && {
+ includePrereleases: true,
+ // The nightly tag name never changes, so the re-uploaded APK's date is the
+ // only usable version, which in turn needs pseudo-versioning.
+ useLatestAssetDateAsReleaseDate: true,
+ releaseDateAsVersion: true,
+ versionDetection: false,
+ }),
+};
+
+const config = {
+ apps: [
+ {
+ id: 'moe.sable.client',
+ url: `https://github.com/${GITHUB_REPO}`,
+ author: 'SableClient',
+ name: 'Sable',
+ installedVersion: null,
+ latestVersion: version,
+ apkUrls: JSON.stringify([[APK_NAME, APK_URL]]),
+ otherAssetUrls: '[]',
+ preferredApkIndex: 0,
+ additionalSettings: JSON.stringify(additionalSettings),
+ lastUpdateCheck: null,
+ pinned: false,
+ categories: ['Communication'],
+ releaseDate: null,
+ changeLog: null,
+ overrideSource: 'GitHub',
+ allowIdChange: false,
+ pendingRepoRenameUrl: null,
+ },
+ ],
+};
+
+writeFileSync(resolve(outputPath), JSON.stringify(config, null, 2) + '\n');
+console.log(`Generated ${outputPath}`);