diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9a96827cc60adef32218cc1c1411b83740f26583..6a5ecca0da8d0f534a10d935d07f878a02fa37ec 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -51,7 +51,7 @@ variables:
     - .\.venv\Scripts\Activate.ps1
     - pip install --upgrade llbase autobuild certifi
   script:
-    - autobuild configure -c Release -- -DUSE_LTO=ON -DDISABLE_FATAL_WARNINGS=ON -DREVISION_FROM_VCS=FALSE
+    - autobuild configure -c Release -- -DUSE_LTO=ON -DDISABLE_FATAL_WARNINGS=ON -DREVISION_FROM_VCS=FALSE -DENABLE_DELTA_GEN=ON
     - autobuild build -c Release --no-configure
     - autobuild graph -c Release --graph-file alchemy-windows${AUTOBUILD_ADDRSIZE}-dependencies.svg build-vc-64/autobuild-package.xml
     - $AlchemyPdbPath = Resolve-Path build-vc-*/newview/Release/alchemy-bin.pdb
@@ -71,7 +71,7 @@ variables:
       - build-vc-*/newview/Release/alchemy-bin.exe
       - build-vc-*/newview/Release/alchemy-bin.pdb
       - build-vc-*/newview/Release/alchemy-bin.src.zip
-      - build-vc-*/newview/Release/Alchemy_*_Setup.exe
+      - build-vc-*/newview/Deploy/*
       - alchemy-*-dependencies.svg
 
 .mac_build:
@@ -396,7 +396,7 @@ build:release:windows64:
     - Pop-Location
 
     - Push-Location ./build-vc-64/
-    -   Push-Location ./newview/Release/
+    -   Push-Location ./newview/Deploy/
     -     $WinFileName = Get-ChildItem -Path . -Name -Include Alchemy_*_Setup.exe
     -     $WinFileHash = (Get-FileHash .\$WinFileName -a sha256).Hash
     -     $WinPackageUrl = "${UploadDestURL}/${WinFileName}"
diff --git a/indra/cmake/Variables.cmake b/indra/cmake/Variables.cmake
index 1686547cc79c63761c5ba930af75fb1fd1db40b7..73edcfacc476a3814a05e929e06b3331eed9bef6 100644
--- a/indra/cmake/Variables.cmake
+++ b/indra/cmake/Variables.cmake
@@ -282,6 +282,8 @@ set(USESYSTEMLIBS OFF CACHE BOOL "Use libraries from your system rather than Lin
 
 set(USE_PRECOMPILED_HEADERS ON CACHE BOOL "Enable use of precompiled header directives where supported.")
 
+set(VIEWER_UPDATE_SERVICE "https://update.alchemyviewer.org" CACHE STRING "Update service URL")
+
 source_group("CMake Rules" FILES CMakeLists.txt)
 
 endif(NOT DEFINED ${CMAKE_CURRENT_LIST_FILE}_INCLUDED)
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 8a1f230133864e7807434c0e88ff82f589a5dc88..a640c7ceead7d073bd44fd78890209acff4cc81e 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -2084,6 +2084,12 @@ if (WINDOWS)
           ${CMAKE_CURRENT_SOURCE_DIR}/event_host_manifest.py
         )
 
+      if (ENABLE_DELTA_GEN)
+        set(DELTA_GEN_SETTING "--gendelta=ON")
+      else ()
+        set(DELTA_GEN_SETTING "")
+      endif ()
+
       add_custom_command(
         OUTPUT ${VIEWER_BUILD_DEST_DIR}/touched.bat
         COMMAND ${Python3_EXECUTABLE}
@@ -2105,6 +2111,8 @@ if (WINDOWS)
           --source=${CMAKE_CURRENT_SOURCE_DIR}
           --touch=${VIEWER_BUILD_DEST_DIR}/touched.bat
           --versionfile=${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt
+          --updateurl=${VIEWER_UPDATE_SERVICE}
+          ${DELTA_GEN_SETTING}
         DEPENDS
             ${VIEWER_BINARY_NAME}
             ${CMAKE_CURRENT_SOURCE_DIR}/viewer_manifest.py
diff --git a/indra/newview/alsquirrelupdater.cpp b/indra/newview/alsquirrelupdater.cpp
index 73afe03fc746f78f17cd307c8a1ed8446443c6ad..c958d7c768b8cb694eb1af450c8771349f5cb276 100644
--- a/indra/newview/alsquirrelupdater.cpp
+++ b/indra/newview/alsquirrelupdater.cpp
@@ -27,6 +27,8 @@
 
 #include "alsquirrelupdater.h"
 
+#include "llviewerbuildconfig.h"
+
 #include "llappviewer.h"
 #include "llnotificationsutil.h"
 #include "llversioninfo.h"
@@ -39,6 +41,14 @@
 #include "llwin32headerslean.h"
 #include "llstartup.h"
 
+#if LL_WINDOWS
+#define UPDATER_PLATFORM "windows"
+#define UPDATER_ARCH "x64"
+#elif LL_DARWIN
+#define UPDATER_PLATFORM "macos"
+#define UPDATER_ARCH "universal"
+#endif
+
 static std::string win32_errorcode_to_string(LONG errorMessageID)
 {
 	if (errorMessageID == 0)
@@ -121,7 +131,7 @@ struct ALRegWriter
 void ALUpdateUtils::updateSlurlRegistryKeys(const std::string& protocol, const std::string& name, const std::string& executable_path)
 {
 	// SecondLife slurls
-	std::wstring reg_path = ll_convert_string_to_wide(llformat("Software\\Classes\\%s", protocol.c_str()));
+	std::wstring reg_path = ll_convert_string_to_wide(fmt::format("Software\\Classes\\{}", protocol));
 	if (auto regpath = ALRegWriter(HKEY_CURRENT_USER, reg_path))
 	{
 		regpath.setValue(name);
@@ -143,7 +153,7 @@ void ALUpdateUtils::updateSlurlRegistryKeys(const std::string& protocol, const s
 
 				if (auto command = open.createSubKey(TEXT("command")))
 				{
-					std::string open_cmd_string = llformat("\"%s\" -url \"%s\"", executable_path.c_str(), "%1");
+					std::string open_cmd_string = fmt::format("\"{}\" -url \"%1\"", executable_path);
 					command.setValue(open_cmd_string, REG_EXPAND_SZ);
 				}
 			}
@@ -154,8 +164,6 @@ void ALUpdateUtils::updateSlurlRegistryKeys(const std::string& protocol, const s
 // static
 bool ALUpdateUtils::handleCommandLineParse(LLControlGroupCLP& clp)
 {
-	if(!ALUpdateHandler::isSupported()) return true;
-
 	bool is_install = clp.hasOption("squirrel-install");
 	bool is_update = clp.hasOption("squirrel-updated");
 	bool is_uninstall = clp.hasOption("squirrel-uninstall");
@@ -232,9 +240,9 @@ bool ALUpdateUtils::handleCommandLineParse(LLControlGroupCLP& clp)
 		}
 		LLAppViewer::instance()->removeDumpDir();
 		LLAppViewer::instance()->removeMarkerFiles();
-		return false;
+		return true;
 	}
-	return true;
+	return false;
 }
 
 ALUpdateHandler::ALUpdateHandler()
@@ -250,8 +258,7 @@ ALUpdateHandler::ALUpdateHandler()
 	{
 		std::string channel = LLVersionInfo::instance().getChannel();
 		channel.erase(std::remove_if(channel.begin(), channel.end(), isspace), channel.end());
-
-		mUpdateURL = llformat("http://update.alchemyviewer.net/windows%s/channel/%s/", std::to_string(ADDRESS_SIZE).c_str(), channel.c_str());
+		mUpdateURL = fmt::format("{}/{}/{}/{}/", VIEWER_UPDATE_SERVICE, UPDATER_PLATFORM, UPDATER_ARCH, channel);
 	}
 	LL_INFOS() << "Update service url: " << mUpdateURL << LL_ENDL;
 
@@ -468,8 +475,8 @@ void ALUpdateHandler::updateCheckFinished(const LLSD& data)
 		else if (update_preference == 2)
 		{
 			LLSD args;
-			args["VIEWER_VER"] = llformat("%s %s", LLVersionInfo::instance().getChannel().c_str(), LLVersionInfo::instance().getShortVersion().c_str());
-			args["VIEWER_UPDATES"] = llformat("%s", new_ver.version().c_str());
+			args["VIEWER_VER"] = fmt::format("{} {}", LLVersionInfo::instance().getChannel(), LLVersionInfo::instance().getShortVersion());
+			args["VIEWER_UPDATES"] = fmt::format("{}", new_ver.version());
 			LLSD payload;
 			payload["user_update_action"] = LLSD(E_DOWNLOAD_INSTALL);
 			LLNotificationsUtil::add("UpdateDownloadRequest", args, payload, boost::bind(&ALUpdateHandler::onUpdateNotification, this, _1, _2));
@@ -507,7 +514,7 @@ void ALUpdateHandler::updateDownloadFinished(const LLSD& data)
 			}
 		}
 		LLSD args;
-		args["VIEWER_VER"] = llformat("%s %s", LLVersionInfo::instance().getChannel().c_str(), new_ver.version().c_str());
+		args["VIEWER_VER"] = fmt::format("{} {}", LLVersionInfo::instance().getChannel(), new_ver.version());
 		args["VIEWER_UPDATES"] = releases;
 		LLSD payload;
 		payload["user_update_action"] = LLSD(E_DOWNLOADED);
@@ -524,8 +531,8 @@ void ALUpdateHandler::updateInstallFinished(const LLSD& data)
 		ALVersionInfo new_ver;
 		if (mSavedUpdateInfo.has("futureVersion")) new_ver.parse(mSavedUpdateInfo["futureVersion"].asString());
 		LLSD args;
-		args["VIEWER_VER"] = llformat("%s %s", LLVersionInfo::instance().getChannel().c_str(), LLVersionInfo::instance().getShortVersion().c_str());
-		args["VIEWER_UPDATES"] = llformat("%s", new_ver.version().c_str());
+		args["VIEWER_VER"] = fmt::format("{} {}", LLVersionInfo::instance().getChannel(), LLVersionInfo::instance().getShortVersion());
+		args["VIEWER_UPDATES"] = fmt::format("{}", new_ver.version());
 		LLSD payload;
 		payload["user_update_action"] = LLSD(E_INSTALLED_RESTART);
 		LLNotificationsUtil::add((LLStartUp::getStartupState() < STATE_STARTED ? "UpdateInstalledRestart" : "UpdateInstalledRestartToast"), args, payload, boost::bind(&ALUpdateHandler::onUpdateNotification, this, _1, _2));
diff --git a/indra/newview/installers/windows/placeholder.gif b/indra/newview/installers/windows/placeholder.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fe374f1c6176cf7f82bd49741a2b6fb7721ac12d
Binary files /dev/null and b/indra/newview/installers/windows/placeholder.gif differ
diff --git a/indra/newview/installers/windows/viewer.nuspec b/indra/newview/installers/windows/viewer.nuspec
index af8fb411ab8028f558f3da4cd2e3a23192ff099d..6325fd4a38deb6f9731d75cab66caccba0657b92 100644
--- a/indra/newview/installers/windows/viewer.nuspec
+++ b/indra/newview/installers/windows/viewer.nuspec
@@ -5,12 +5,12 @@
     <version>${VIEWER_VERSION_MAJOR}.${VIEWER_VERSION_MINOR}.${VIEWER_VERSION_PATCH}</version>
     <authors>Alchemy Development Group</authors>
     <owners>Alchemy Development Group</owners>
-	<title>${VIEWER_CHANNEL}</title>
+  	<title>${VIEWER_CHANNEL}</title>
     <description>${VIEWER_CHANNEL}</description>
     <copyright>Copyright (C) 2013-2023 Alchemy Development Group</copyright>
 	<projectUrl>https://www.alchemyviewer.org</projectUrl>
   </metadata>
   <files>
-    <file src="Release\**\*.*" target="lib\net45\" exclude="Release\**\*.pdb;Release\**\*.vshost.*;Release\**\*.nsi;Release\**\*.bat;Release\**\*_Setup.exe;Release\**\*-bin.exe"/>
+    <file src="Release\**\*.*" target="lib\net45\" exclude="Release\**\*.pdb;Release\**\*.vshost.*;Release\**\*.nsi;Release\**\*.bat;Release\**\*_Setup.exe;Release\**\*-bin.exe;Release\**\*.exp;Release\**\*.lib"/>
   </files>
 </package>
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 6f5fe3352cea4d73d7d9f9435e486a435890d9e5..3aa83d8c981d0179875b66028bf04d15bc7db006 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -2560,7 +2560,7 @@ bool LLAppViewer::initConfiguration()
 	}
 
 #if LL_WINDOWS
-	if (ALUpdateHandler::isSupported() && !ALUpdateUtils::handleCommandLineParse(clp))
+	if (ALUpdateUtils::handleCommandLineParse(clp))
 	{
 		return false;
 	}
diff --git a/indra/newview/llviewerbuildconfig.h.in b/indra/newview/llviewerbuildconfig.h.in
index a408ccd56a508c8f3dcb7e3f4602a521caeeaefe..4e975df04b02a68b54fc18cd1252760f3f7099ed 100644
--- a/indra/newview/llviewerbuildconfig.h.in
+++ b/indra/newview/llviewerbuildconfig.h.in
@@ -42,6 +42,9 @@
 #define LL_VIEWER_VERSION_MINOR @VIEWER_VERSION_MINOR@
 #define LL_VIEWER_VERSION_PATCH @VIEWER_VERSION_PATCH@
 
+// Updater URL
+#define VIEWER_UPDATE_SERVICE "@VIEWER_UPDATE_SERVICE@"
+
 // Sentry
 #define SENTRY_DSN "@SENTRY_DSN@"
 
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index d7b4f56e2b061b4f5db025541fca7952585c80bf..139b93d1c6220a06d20976fda2c24936f8c7960d 100755
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -617,22 +617,46 @@ def package_finish(self):
                 '-Properties', 'NoWarn=NU5128',
                 os.path.join(self.args['build'], 'viewer.nuspec')])
 
+        #Build installer and update delta
         squirrel_exe = os.path.join(self.args['build'], os.pardir, 'packages', 'squirrel', 'Squirrel.exe')
+
+        # Download previous build for delta generation
+        temp_installdir = os.path.join(self.args['build'], 'Installer')
+        if 'gendelta' in self.args and 'updateurl' in self.args:
+            if (self.address_size == 64):
+                updater_arch = 'x64'
+            else:
+                updater_arch = 'x86'
+
+            self.run_command(
+                [squirrel_exe,
+                    'http-down',
+                    '--releaseDir', temp_installdir,
+                    '--url', '{}/windows/{}/{}/'.format(self.args['updateurl'], updater_arch, self.app_name_oneword())])
+
+        # Build installer files
+        temp_nupkg = os.path.join(self.args['build'], '{}.{}.nupkg'.format(self.app_name_oneword(), '.'.join(self.args['version'])))
         self.run_command(
             [squirrel_exe,
                 'releasify',
-                '--releaseDir', os.path.join(self.args['build'], 'Releases'),
+                '--releaseDir', temp_installdir,
                 '--framework', 'vcredist143-x64',
                 '--icon', os.path.join(self.args['source'], 'installers', 'windows', 'install_icon.ico'),
-                '--splashImage', os.path.join(self.args['source'], 'installers', 'windows', 'splash.gif'),
-                '--package', os.path.join(self.args['build'], '{}.{}.nupkg'.format(self.app_name_oneword(), '.'.join(self.args['version'])))])
+                '--splashImage', os.path.join(self.args['source'], 'installers', 'windows', 'placeholder.gif'),
+                '--package', temp_nupkg])
 
+        # Copy to final installer destination
         installer_file = self.installer_base_name() + '_Setup.exe'
+        with self.prefix(src=temp_installdir, dst=os.path.join(self.args['build'], 'Deploy')):  # everything goes in Contents
+            self.path(src=self.app_name_oneword() + 'Setup.exe', dst=installer_file)
+            self.path('{}-{}-*.nupkg'.format(self.app_name_oneword(), '.'.join(self.args['version'])))
+            self.path('RELEASES')
 
-        os.rename(os.path.join('Releases', self.app_name_oneword() + 'Setup.exe'), os.path.join('Release', installer_file))
+        # Clean up temporary files
+        os.remove(temp_nupkg)
+        shutil.rmtree(temp_installdir, True)
 
-        self.sign(installer_file)
-        self.created_path(self.dst_path_of(installer_file))
+        self.created_path(os.path.join(self.args['build'], 'Deploy', installer_file))
         self.package_file = installer_file
 
     def sign(self, exe):