diff --git a/Desktop/QSFERA.cmake b/Desktop/QSFERA.cmake
index 5034b3e6..6ac446a6 100644
--- a/Desktop/QSFERA.cmake
+++ b/Desktop/QSFERA.cmake
@@ -5,6 +5,7 @@ set( APPLICATION_VENDOR "QSfera" )
set( APPLICATION_ICON_NAME "qsfera" )
set( APPLICATION_REV_DOMAIN "eu.qsfera.desktop" )
set( APPLICATION_DEFAULT_SERVER_URL "https://qsfera.kusoft.xyz" )
+set( APPLICATION_UPDATE_URL "https://qsfera.kusoft.xyz/desktop/updates/stable.xml" )
if(BETA_CHANNEL_BUILD)
set( APPLICATION_NAME "${APPLICATION_NAME} Beta")
diff --git a/Desktop/docs/windows-update-feed.md b/Desktop/docs/windows-update-feed.md
new file mode 100644
index 00000000..720b85c6
--- /dev/null
+++ b/Desktop/docs/windows-update-feed.md
@@ -0,0 +1,47 @@
+# Windows Update Feed
+
+QSfera Desktop uses the built-in Windows updater from `src/gui/updater`.
+The updater reads `APPLICATION_UPDATE_URL`, appends query parameters such as
+`version`, `platform=win32`, and `msi=true`, and expects an XML document with
+root element `QSfera`.
+
+Production feed URL:
+
+```text
+https://qsfera.kusoft.xyz/desktop/updates/stable.xml
+```
+
+The no-update feed is valid when no signed Windows MSI is available yet:
+
+```xml
+
+
+
+
+ https://qsfera.kusoft.xyz/
+
+
+```
+
+To advertise a signed MSI update, publish the MSI over HTTPS and replace the
+feed with a concrete version:
+
+```xml
+
+
+ 1.3.7
+ 1.3.7
+ https://qsfera.kusoft.xyz/
+ https://qsfera.kusoft.xyz/desktop/downloads/QSfera_Desktop-1.3.7-x64.msi
+
+```
+
+Validate the live feed before a Windows production release:
+
+```powershell
+./scripts/validate-desktop-update-feed.ps1 -Url "https://qsfera.kusoft.xyz/desktop/updates/stable.xml"
+./scripts/validate-production-release.ps1 -VerifyUpdateFeed
+```
+
+For a stable signed tag release, `validate-production-release.ps1 -RequireSigning`
+also verifies this live feed.
diff --git a/Desktop/scripts/validate-desktop-update-feed.ps1 b/Desktop/scripts/validate-desktop-update-feed.ps1
new file mode 100644
index 00000000..213f8c8d
--- /dev/null
+++ b/Desktop/scripts/validate-desktop-update-feed.ps1
@@ -0,0 +1,85 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$Url
+)
+
+$ErrorActionPreference = "Stop"
+
+function Fail($Message) {
+ Write-Error $Message
+ exit 1
+}
+
+$parsedUrl = $null
+if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$parsedUrl)) {
+ Fail "Desktop update feed URL is not absolute: $Url"
+}
+
+if ($parsedUrl.Scheme -ne "https") {
+ Fail "Desktop update feed URL must use HTTPS: $Url"
+}
+
+try {
+ $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 30
+} catch {
+ Fail "Desktop update feed request failed for ${Url}: $($_.Exception.Message)"
+}
+
+if ($response.StatusCode -ne 200) {
+ Fail "Desktop update feed must return HTTP 200. Actual status: $($response.StatusCode)"
+}
+
+try {
+ [xml]$xml = $response.Content
+} catch {
+ Fail "Desktop update feed is not valid XML: $($_.Exception.Message)"
+}
+
+if ($xml.DocumentElement.Name -ne "QSfera") {
+ Fail "Desktop update feed root element must be ."
+}
+
+$requiredElements = @("version", "versionstring", "web", "downloadurl")
+foreach ($name in $requiredElements) {
+ if ($null -eq $xml.QSfera.$name) {
+ Fail "Desktop update feed is missing <$name>."
+ }
+}
+
+$version = [string]$xml.QSfera.version
+$versionString = [string]$xml.QSfera.versionstring
+$web = [string]$xml.QSfera.web
+$downloadUrl = [string]$xml.QSfera.downloadurl
+
+if (-not [string]::IsNullOrWhiteSpace($web)) {
+ $webUri = $null
+ if (-not [System.Uri]::TryCreate($web, [System.UriKind]::Absolute, [ref]$webUri) -or $webUri.Scheme -ne "https") {
+ Fail "Desktop update feed must be an absolute HTTPS URL when present."
+ }
+}
+
+if ([string]::IsNullOrWhiteSpace($version)) {
+ if (-not [string]::IsNullOrWhiteSpace($downloadUrl)) {
+ Fail "Desktop no-update feed must leave empty when is empty."
+ }
+} else {
+ $parsedVersion = $null
+ if (-not [System.Version]::TryParse($version, [ref]$parsedVersion)) {
+ Fail "Desktop update feed must be parseable as a version number."
+ }
+
+ if ([string]::IsNullOrWhiteSpace($versionString)) {
+ Fail "Desktop update feed must set when is present."
+ }
+
+ $downloadUri = $null
+ if (-not [System.Uri]::TryCreate($downloadUrl, [System.UriKind]::Absolute, [ref]$downloadUri) -or $downloadUri.Scheme -ne "https") {
+ Fail "Desktop update feed must be an absolute HTTPS URL when an update is advertised."
+ }
+
+ if (-not $downloadUri.AbsolutePath.EndsWith(".msi", [System.StringComparison]::OrdinalIgnoreCase)) {
+ Fail "Desktop Windows updater expects an MSI download URL."
+ }
+}
+
+Write-Host "QSfera Desktop update feed validation passed: $Url"
diff --git a/Desktop/scripts/validate-production-release.ps1 b/Desktop/scripts/validate-production-release.ps1
index 92369d33..63acfe9c 100644
--- a/Desktop/scripts/validate-production-release.ps1
+++ b/Desktop/scripts/validate-production-release.ps1
@@ -1,5 +1,6 @@
param(
[switch]$RequireSigning,
+ [switch]$VerifyUpdateFeed,
[string]$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
)
@@ -35,6 +36,10 @@ if ($branding -notmatch 'APPLICATION_DEFAULT_SERVER_URL\s+"https://qsfera\.kusof
Fail "Desktop default server URL must be https://qsfera.kusoft.xyz."
}
+if ($branding -notmatch 'APPLICATION_UPDATE_URL\s+"https://qsfera\.kusoft\.xyz/desktop/updates/stable\.xml"') {
+ Fail "Desktop update feed URL must be https://qsfera.kusoft.xyz/desktop/updates/stable.xml."
+}
+
if ($cmake -notmatch 'option\(WITH_UPDATE_NOTIFICATION\s+"Whether to check for new releases"\s+ON\)') {
Fail "Desktop release builds must keep update notifications enabled by default."
}
@@ -55,6 +60,17 @@ if ($RequireSigning) {
if ([string]::IsNullOrWhiteSpace($env:CRAFT_CODESIGN_CERTIFICATE)) {
Fail "Production Windows tag releases require CRAFT_CODESIGN_CERTIFICATE."
}
+
+ $VerifyUpdateFeed = $true
+}
+
+if ($VerifyUpdateFeed) {
+ $feedValidator = Join-Path $PSScriptRoot "validate-desktop-update-feed.ps1"
+ if (-not (Test-Path -LiteralPath $feedValidator)) {
+ Fail "Desktop update feed validator was not found: $feedValidator"
+ }
+
+ & $feedValidator -Url "https://qsfera.kusoft.xyz/desktop/updates/stable.xml"
}
Write-Host "QSfera Desktop production release validation passed."
diff --git a/PRODUCTION_READINESS.md b/PRODUCTION_READINESS.md
index 49534b97..58020db5 100644
--- a/PRODUCTION_READINESS.md
+++ b/PRODUCTION_READINESS.md
@@ -36,6 +36,7 @@ Date: 2026-06-08.
- Server Docker builds now accept explicit production metadata for `QSFERA_VERSION`, `QSFERA_EDITION`, and `QSFERA_BUILD_DATE`, and the Raspberry Pi 5 compose path defaults to pinned image `qsfera-cloud-server:7.0.0-rpi5-stable`.
- Windows/Desktop update channel defaults now match the build channel: stable builds default to `stable`, beta builds default to `beta`.
- Windows/Desktop CI now validates production release invariants before Windows packaging and requires signing variables for stable tag releases.
+- Windows/Desktop now has a configured HTTPS update feed at `https://qsfera.kusoft.xyz/desktop/updates/stable.xml`; the feed returned HTTP `200` on 2026-06-08 and contains the XML elements parsed by `Desktop/src/gui/updater/updateinfo.cpp`.
## Raspberry Pi 5 Check
@@ -65,7 +66,7 @@ Date: 2026-06-08.
- Fill production domains, ACME email, admin passwords, Keycloak passwords, and storage secrets from a secret manager. Do not commit real values.
- Run `./validate-production-env.sh .env` and then `docker compose config` for `Server/devtools/deployments/qsfera_full` with a filled production `.env`; Docker CLI is not available in this local environment, so compose rendering is not locally verified.
- Provide real Windows signing secrets in CI: `QSFERA_DESKTOP_SIGN_PACKAGE=True` and `QSFERA_DESKTOP_CODESIGN_CERTIFICATE` with the Craft-compatible signing certificate reference.
-- Configure and verify the desktop updater endpoint via `APPLICATION_UPDATE_URL`; `Desktop/scripts/validate-production-release.ps1` confirms update notifications are enabled but cannot prove a live update feed without that URL.
+- Publish the first signed Windows MSI and update `https://qsfera.kusoft.xyz/desktop/updates/stable.xml` from the current no-update XML to a concrete ``, ``, and HTTPS `.msi` ``.
## Remaining P1