diff --git a/docs/superpowers/specs/2026-09-27-in-app-updates-design.md b/docs/superpowers/specs/2026-09-27-in-app-updates-design.md
new file mode 100644
index 0000000..1f1097f
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-27-in-app-updates-design.md
@@ -0,0 +1,158 @@
+# In-App Updates — Design
+
+Date: 2026-09-27
+Status: approved, pending implementation plan
+
+## Background
+
+KwabaTV has no update mechanism today — every new build requires a manual
+`adb install -r` sideload from a development machine. That's fine for this
+session's iteration pace, but not sustainable once the app is "done" and
+just sitting on a Fire TV Stick: any future fix requires physically
+reconnecting ADB.
+
+Kwaba Sync's own desktop Electron app (`desktop/`, in the `kwaba-sync`
+repo) already solved an equivalent problem: the server proxies the latest
+GitHub-style release from Gitea (`src/desktop-release.js`), gated behind a
+login because that repository is private, and the Electron app polls it,
+shows an in-window update banner, and never installs without an explicit
+click.
+
+KwabaTV's own Gitea repo (`GGB/KwabaTV`) is public — confirmed via its
+Gitea API (`visibility":"public"`, releases endpoint returns 200
+unauthenticated). That removes the reason the desktop app's design needed a
+server-side proxy at all (private-repo auth). This design is intentionally
+simpler than desktop's as a result: no `kwaba-sync` involvement, no
+server-side changes, no coordination needed with that repo's owner. It
+mirrors desktop's *behavior* (check on launch, never install silently,
+graceful failure) without mirroring its *architecture*.
+
+## Goal
+
+The app checks Gitea for a newer release on every launch, and — if one
+exists — offers to download and install it, entirely on-device.
+
+## Release convention
+
+- `versionName` in `app/build.gradle.kts` moves from the current `"1.0"`
+ to a proper 3-part semver (e.g. `"1.1.0"`) as part of this work, since
+ the comparison logic below assumes major/minor/patch on both sides.
+- Tag format: `vX.Y.Z` (e.g. `v1.1.0`), matching `versionName`.
+- Exactly one APK asset per release, always named `KwabaTV.apk`, so the
+ app never has to guess which asset to download.
+- Releases are published manually for now (no CI in this repo yet).
+ Drafts and pre-releases are ignored by the version check.
+
+## Components
+
+All new code lives in kwabatv; nothing else changes outside it.
+
+**`UpdateChecker.kt`** — a small, self-contained class with one entry
+point, e.g. `checkForUpdate(context, onAvailable: (UpdateInfo) -> Unit)`.
+On a background thread, it does a plain HTTP GET to
+`https://git.whodovoodoo.de/api/v1/repos/GGB/KwabaTV/releases`, picks the
+highest-versioned non-draft, non-prerelease release whose tag matches
+`vX.Y.Z`, and compares it numerically (major/minor/patch) against
+`BuildConfig.VERSION_NAME`. If the release is newer, it calls back with
+`{ version, apkDownloadUrl }` on the main thread. Any failure along the
+way (network unreachable, non-200, malformed JSON, no matching release) is
+swallowed silently — no error UI, no retry logic beyond "try again next
+launch."
+
+**`MainActivity.onCreate()`** calls `UpdateChecker.checkForUpdate(...)`
+right after `webView.loadUrl(BuildConfig.WEBSITE_URL)`, so the check never
+delays getting to content. If the callback fires, it shows a native
+`AlertDialog`:
+
+> **KwabaTV v1.1.0 ist verfügbar**
+> [Aktualisieren] [Später]
+
+Android's default dialog buttons are focusable and DPAD-navigable with no
+extra work. "Später" just dismisses; the same check runs again next
+launch, with no persisted "skip this version" state.
+
+**Download + install**, triggered by "Aktualisieren":
+
+- Uses `android.app.DownloadManager` (a base-SDK system service — no
+ AndroidX needed, keeping the app's existing zero-dependency footprint)
+ to download the APK asset directly from Gitea's public release URL.
+- Registers a `BroadcastReceiver` for
+ `DownloadManager.ACTION_DOWNLOAD_COMPLETE` to know when the download
+ finishes.
+- On completion, calls `DownloadManager.getUriForDownloadedFile(id)` — this
+ returns a ready-to-use `content://` URI with no custom `FileProvider`
+ setup required (the earlier draft of this design needed one, which
+ would have pulled in an AndroidX dependency; `DownloadManager` avoids
+ that entirely).
+- Fires `Intent(ACTION_VIEW)` with that URI, MIME type
+ `application/vnd.android.package-archive`, and
+ `FLAG_GRANT_READ_URI_PERMISSION`, which opens Android's own system
+ package installer. The user taps "Install" themselves inside that
+ system UI — Android does not allow silent unattended installs, by OS
+ design, regardless of permissions granted.
+
+**First-run permission gate:** before firing the install intent, check
+`packageManager.canRequestPackageInstalls()`. If false (first time ever
+installing an update), send the user to
+`Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES` for this package instead, so
+they can grant "install unknown apps" for KwabaTV specifically. They then
+press "Aktualisieren" again to retry.
+
+**New manifest entries:**
+- ``
+- No new `` entry (avoided by using `DownloadManager`, see
+ above).
+
+## Data flow
+
+```
+App launch
+ → webView.loadUrl(WEBSITE_URL) [not blocked by the check]
+ → UpdateChecker (background thread)
+ GET git.whodovoodoo.de/.../releases
+ → newer release found?
+ no → nothing happens
+ yes → AlertDialog on main thread
+ "Später" → nothing further this launch
+ "Aktualisieren" → canRequestPackageInstalls()?
+ no → Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
+ yes → DownloadManager.enqueue(apk)
+ → ACTION_DOWNLOAD_COMPLETE
+ → ACTION_VIEW install intent
+ → system installer UI (user taps Install)
+```
+
+## Error handling
+
+Every failure mode in the check itself (DNS/network failure, non-200,
+malformed JSON, no releases, no tag matching `vX.Y.Z`) results in no
+dialog and no visible change — identical to a launch where the app is
+already current. This matches desktop's graceful-degradation philosophy:
+an update check is a nice-to-have, never something that should be able to
+block or degrade normal app use.
+
+Download failures (`DownloadManager` reports a failed status) are also
+silent for now — the user can just press "Aktualisieren" again next
+launch, or manually sideload as a fallback (this doesn't remove that
+option). A visible failure toast is a reasonable future improvement, not
+required for the first version of this feature.
+
+## Testing
+
+This feature cannot be meaningfully unit-tested in isolation — the
+interesting behavior is entirely about real network calls, real Android
+permission state, and the real system package installer. It will be
+verified end-to-end on the real Fire TV Stick used throughout this
+project: publish an actual test release on the `GGB/KwabaTV` Gitea repo,
+then run the full check → dialog → download → permission-gate →
+install flow on-device, the same hands-on verification approach used for
+every other feature this session.
+
+## Out of scope (for this pass)
+
+- Any `kwaba-sync` server changes (Option B from the design discussion —
+ hosting the APK for discovery from the Kwaba website's lobby — is a
+ separate, later concern, not required for in-app updates to work).
+- CI/automated release publishing.
+- Periodic re-checks while the app stays open (check is launch-only).
+- "Skip this version" / persisted dismissal state.