Signed, self-hosted updates for .NET applications
Features · Installation · Usage · Examples · Migrating to 5.0 · Building
nUpdate keeps your .NET application up to date from your own web server. You build, sign and publish update packages with nUpdate Administration; the nUpdate library in your application finds them, downloads and verifies them and hands them to the installer, with a ready-made user interface or one of your own.
![]() |
![]() |
| Packages of a project | Creating an update package |
![]() |
![]() |
| Download statistics | Migration assistant for projects of nUpdate 4 |
In your application
- Task-based asynchronous API with cancellation and progress reporting
- Every package is checked against its size, SHA-512 hash and RSA-PSS signature (PEM keys) before it is installed
- Built-in update dialogs for Windows Forms, WPF and Avalonia, or your own user interface on top of the same update flow
- An installer for Windows, Linux and macOS that needs no .NET on the machine, with a small window or none at all, and replaces macOS application bundles as a whole
- One package file per platform, so a version can ship a Windows, a Linux and a macOS build side by side
- Pre-release channels, rollout conditions and necessary updates
- Operations during the installation: files and processes everywhere, registry and services on Windows
- Very large update packages
In nUpdate Administration
- Runs on Windows, Linux and macOS, in light or dark mode as the system is set; on macOS as an app with its own menus
- A project is a folder you can copy to any machine
- Published packages stay editable: change their files, operations and platforms, and nUpdate Administration builds the changed platforms again, signs them and replaces them on the server
- Publishing over SFTP, FTPS and FTP, or through your own transfer plugin
- Secrets encrypted with a project password, server certificates and host keys trusted by fingerprint
- Download statistics through a small PHP/MySQL endpoint
- A migration assistant that moves projects of nUpdate 3 and 4 over while both versions keep running
| Component | Runs on |
|---|---|
nUpdate library |
.NET Standard 2.0: .NET Framework 4.6.2 and later, .NET 6 and later |
Built-in installer (nUpdate.UpdateInstaller.UI.Avalonia) |
Self-contained, nothing to install: win-x64, win-x86, win-arm64, linux-x64, linux-arm64, osx-x64, osx-arm64 |
nUpdate.UpdateInstaller, the base for an installer of your own |
.NET Standard 2.0 |
nUpdate.UI.WindowsForms, nUpdate.UI.WPF |
.NET Framework 4.6.2 and .NET 8 on Windows |
nUpdate.UI.Avalonia |
.NET 8 on Windows, Linux and macOS, in an application on Avalonia 12 with the Fluent theme |
| nUpdate Administration | .NET 10 on Windows, Linux and macOS |
| Update server | Any web server for the files; PHP 7.1+ with mysqli and MySQL for the optional statistics |
nUpdate 5 is a release candidate: 5.0.0-rc.1 is complete and tested, but details may still change before 5.0.0.
dotnet add package nUpdate --version 5.0.0-rc.1
dotnet add package nUpdate.UpdateInstaller.UI.Avalonia --version 5.0.0-rc.1
nUpdate.UpdateInstaller.UI.Avalonia carries the built-in installer for seven runtime identifiers. On build it copies
the one of your application's RuntimeIdentifier to nUpdate.Installer/<rid>/ next to your application, which is
where UpdateManager looks for it. Ship that folder with your application. A project without a runtime identifier
(a framework-dependent build, or .NET Framework) gets all seven, about 170 MB, and warning NUPD001; name the ones you
ship instead:
<PropertyGroup>
<nUpdateInstallerRuntimes>win-x64;win-arm64</nUpdateInstallerRuntimes>
</PropertyGroup>nUpdate Administration is available from the releases.
[assembly: ApplicationVersion("1.0.0")]You can also pass the version to the UpdateManager constructor (currentVersion); the constructor argument wins when
both are present.
var manager = new UpdateManager(new Uri("https://example.com/updates/nupdate.json"), publicKey,
CultureInfo.CurrentUICulture);publicKey is the PEM public key of your project, and nupdate.json is the feed nUpdate Administration writes next to
the packages/ folder. The administration generates this snippet for your project (Overview > Copy source).
The installer updates the folder of the running executable and starts it again; UpdateManager takes its path from
the process, single-file builds included. Where that fails, for example in a host that runs your code in another
process, set ApplicationExecutablePath to the absolute path of the executable, such as Environment.ProcessPath.
dotnet add package nUpdate.UI.WindowsForms --version 5.0.0-rc.1
dotnet add package nUpdate.UI.WPF --version 5.0.0-rc.1
dotnet add package nUpdate.UI.Avalonia --version 5.0.0-rc.1
Each package provides an UpdaterUI class. Create and use it on the UI thread:
var updaterUI = new UpdaterUI(manager, this); // the owning form or window is optional
updaterUI.UseHiddenSearch = true; // say nothing when the application is up to date
var result = await updaterUI.RunAsync();The user interfaces are thin presenters over nUpdate.Ui.UpdateFlow, which you can also drive with your own dialogs by
implementing IUpdateFlowPresenter.
if (await manager.CheckForUpdatesAsync())
{
await manager.DownloadAsync(progress, cancellationToken);
if (await manager.VerifyAsync())
manager.StartInstaller();
}- Asynchronous: every operation accepts a
CancellationToken;DownloadAsyncreportsUpdateDownloadProgress. - Checked:
DownloadAsynccompares the size and SHA-512 hash of every package with the feed, andVerifyAsyncchecks the RSA-PSS signatures and that each package's manifest names your project and its version. - Timeouts:
HttpTimeout(100 seconds by default) bounds every request, and a server that stops responding surfaces as anHttpRequestException, not as a cancellation. On .NET Framework the timeout covers the headers of a download but not the bytes of the package, so pass aCancellationTokenif a stalled download must be stopped. - Pre-releases:
MinimumStability(Release,ReleaseCandidate,BetaorAny) is the least stable version a client installs, each level including the more stable ones, andAcceptedPreReleaseLabelsadds custom labels. The defaultReleaseinstalls releases only, so release candidates are skipped unless the client asks for them (nUpdate 3 and 4 always offered them). - Rollout and installation:
RolloutConditionsare matched against the rollout conditions of a package, andDefaultAfterInstalltells the installer whether to restart, close or keep the application running. A package can ask to restart the application or to leave it closed instead (set in nUpdate Administration); when several packages are installed together, leaving it closed wins.AfterInstallis the outcome for the updates found, and the update dialogs say when the application stays closed. - Statistics: failures never abort the download; they are written to
UpdateManagerServices.Logger(anILogger).
StartInstaller() copies the installer to a temp folder, writes its options next to it, starts it and closes your
application (unless AfterInstall is KeepRunning). The installer waits for the application to exit, runs the
operations of every package, copies its files, restarts the application and deletes the downloads.
- A window or none: the installer shows a small window with the progress, asks what to do with a file another
process holds open and shows errors. Without a display (a Windows service, a Linux server without
DISPLAYorWAYLAND_DISPLAY), or when the window cannot open, it installs without one;ShowInstallerWindow = falseasks for that.InstallerIcon(a PNG) andInstallerAccentColor(#RRGGBB, also used by thenUpdate.UI.Avaloniadialogs) brand the window, which follows the system's light or dark mode. When the installer cannot even start, for example because the options do not match its version, it says so in the window too. - The log: every run writes
install.loginto the installer's temp folder (<temp>/nUpdate Installer/<application>, or a folder next to it with a suffix while an earlier installer or a virus scanner still holds a file in it); error messages name it, and on Windows failures of a windowless run also go to the event log. - Rights: on Windows the installer asks for administrator rights through UAC unless
RunInstallerAsAdminisfalse. Linux and macOS have no such prompt: the installer runs as the user, andStartInstaller()throws anUnauthorizedAccessExceptionwith a translated message when that user may not change the application's folder. - Platforms: nUpdate Administration builds one package file per platform:
any, an operating system (win,linux,osx) or a runtime identifier (win-x64,linux-arm64,osx-arm64, ...). A client takes the file of its runtime identifier, else the one of its operating system, else the one for any platform, and does not see versions without a file for it.UpdateManager.Platformis the process's runtime identifier. Publish an AnyCPU build, which runs as a 64-bit process on some machines and as a 32-bit one on others, forwin. Registry and service operations exist only on Windows. - Your own installer: set
InstallerPathto an installer of your own (see the example); give it a folder of its own, since that whole folder is copied.
When the application runs from a bundle (MyApp.app/Contents/MacOS/MyApp), Program and %program% mean the whole
bundle. Add the signed .app folder to Program on a macOS platform in nUpdate Administration; the installer builds
the new bundle next to the installed one as MyApp.app.new, swaps the two in one step and deletes the old one, so the
signature of what you shipped stays intact. Packages cannot contain symbolic links, so a bundle with embedded
frameworks (Versions/Current) has to be flattened first.
codesign keeps the signature of a file in Contents/MacOS that is not Mach-O code, such as a .NET assembly next to
the executable, in extended attributes, which a zip cannot hold. nUpdate Administration therefore writes these
attributes (com.apple.cs.*) into the package's manifest, and the installer sets them on the new bundle before the
swap. Only a Mac can read them: create the package of a macOS bundle in nUpdate Administration on a Mac, or publish
your application as a single file, so that Contents/MacOS holds only Mach-O code. When you change files of a
published package later, the ones you keep keep their attributes.
The built-in installer for osx-x64 and osx-arm64 is signed ad hoc. When you sign and notarize your application,
sign the installer as part of the bundle:
codesign --force --options runtime --timestamp --entitlements installer.entitlements \
--sign "Developer ID Application: …" \
MyApp.app/Contents/MacOS/nUpdate.Installer/osx-arm64/nUpdate.UpdateInstaller.UI.Avalonia
codesign --force --options runtime --timestamp --sign "Developer ID Application: …" MyApp.app
xcrun notarytool submit MyApp.zip --keychain-profile "notary" --wait
Under the hardened runtime a .NET application needs the JIT entitlement, and the installer loads its native libraries
(Skia, HarfBuzz) from where the single file extracts them, which library validation would refuse. installer.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key><true/>
<key>com.apple.security.cs.disable-library-validation</key><true/>
</dict>
</plist>UpdateManager takes an optional UpdateManagerServices with the HttpClient, file system
(System.IO.Abstractions, which the package depends on), process launcher, file permissions, system and application
information and the IApplicationTerminator that closes the host before the installer runs. Replace them to integrate with your own
hosting model or to test your update code without touching the network or the disk.
Versions follow semantic versioning with an optional fourth number:
| Form | Example |
|---|---|
| Release | 2.1.0 |
| With a fourth number (only when it is not 0) | 2.1.0.4 |
| Pre-release | 1.2.0-beta.1, 2.0.0-rc.2, 1.5.0-preview.3 |
| Build metadata (ignored for ordering) | 1.2.0-beta.1+build.7 |
This canonical spelling is what ToString() returns and the only one that parses, so every version has exactly one
form in the feed, the manifests, the project files and the administration. 1.2, 1.2.0.0 and the classic 1.2b1 of
nUpdate 3 and 4 are rejected with a message that shows the expected form; nUpdate Administration converts them when it
migrates a project (1.2.0.0b1 becomes 1.2.0-beta.1). Labels order like SemVer identifiers (numbers before words,
words alphabetically, a shorter label first), so alpha < beta < rc < release.
Each example assumes a manager created as in Usage. They compile against the nUpdate package as shown.
Check quietly on start-up and only speak up when there is something new
// Windows Forms: in the Shown event of the main form. WPF works the same with the window as owner.
var updaterUI = new UpdaterUI(manager, this) { UseHiddenSearch = true };
var result = await updaterUI.RunAsync();
// NoUpdates and Failed searches stay silent with UseHiddenSearch; InstallerStarted closes the application.Call it again from a timer (every few hours, for example) for applications that run for days.
Update a console or headless application with progress output
using var manager = new UpdateManager(new Uri("https://example.com/updates/nupdate.json"), publicKey);
if (!await manager.CheckForUpdatesAsync())
{
Console.WriteLine("You are up to date.");
return;
}
Console.WriteLine($"{manager.AvailableUpdates.Count} update(s), {manager.TotalDownloadSize / 1024.0 / 1024.0:0.0} MB");
var progress = new Progress<UpdateDownloadProgress>(p => Console.Write($"\rDownloading... {p.Percentage:0}%"));
await manager.DownloadAsync(progress);
Console.WriteLine();
if (!await manager.VerifyAsync())
{
Console.WriteLine("A package does not carry a valid signature and was deleted.");
return;
}
manager.StartInstaller(); // closes this application unless AfterInstall is KeepRunningShow what is new before downloading
if (await manager.CheckForUpdatesAsync())
{
foreach (var package in manager.AvailableUpdates)
{
Console.WriteLine($"{package.Version} from {package.PublishedAt:d}{(package.Necessary ? " (required)" : "")}");
Console.WriteLine(package.GetChangelog(CultureInfo.CurrentUICulture)); // falls back to English
}
}Offer betas to some users and roll out in stages
// Pre-releases: Release (default), ReleaseCandidate, Beta or Any; Beta also installs release candidates and releases.
manager.MinimumStability = settings.WantsBetas ? Stability.Beta : Stability.Release;
manager.AcceptedPreReleaseLabels.Add("nightly"); // also offer 2.1.0-nightly.7 to this client
// Rollout conditions: a package with conditions (set in nUpdate Administration) is only offered to clients whose
// values match them, so you can ship to one region or ring first.
manager.RolloutConditions["Region"] = "EU";
manager.RolloutConditions["Ring"] = "early-adopters";Restart with arguments and react to a finished update
manager.DefaultAfterInstall = AfterInstall.Restart; // or Close to leave it closed, or KeepRunning; a package can override it
manager.Arguments.Add(new InstallerArgument("--updated", ArgumentCondition.Succeeded));
manager.Arguments.Add(new InstallerArgument("--update-failed", ArgumentCondition.Failed));
// Restarted from the installer, not from a shortcut: pass on the arguments this instance was started with,
// such as a settings file, so the updated application gets them too.
foreach (var argument in Environment.GetCommandLineArgs().Skip(1))
manager.Arguments.Add(new InstallerArgument(argument));
// In Main of the restarted application:
if (args.Contains("--updated"))
ShowWhatsNew();Proxy, HTTP authentication, timeouts and cancellation
// Set these before the first check: they are applied when the HTTP client is created.
manager.Proxy = new WebProxy("http://proxy.example.com:8080") { Credentials = CredentialCache.DefaultCredentials };
manager.HttpAuthenticationCredentials = new NetworkCredential("updates", "secret");
manager.HttpTimeout = TimeSpan.FromSeconds(30);
using var cancellation = new CancellationTokenSource(TimeSpan.FromMinutes(10));
if (await manager.CheckForUpdatesAsync(cancellation.Token))
await manager.DownloadAsync(progress, cancellation.Token);Log what nUpdate does
// Any Microsoft.Extensions.Logging logger; statistics failures and unreadable files are reported here.
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var services = new UpdateManagerServices { Logger = loggerFactory.CreateLogger("nUpdate") };
using var manager = new UpdateManager(new Uri("https://example.com/updates/nupdate.json"), publicKey, services: services);Your own update dialogs
UpdateFlow runs search, confirmation, download, verification and installer start; you only present each step.
sealed class ConsolePresenter : IUpdateFlowPresenter
{
public Task<bool> RunSearchAsync(Func<CancellationToken, Task<bool>> search) => search(CancellationToken.None);
public Task ShowNoUpdatesAsync()
{
Console.WriteLine("You are up to date.");
return Task.CompletedTask;
}
public Task<bool> ConfirmInstallAsync()
{
Console.Write("Install the updates now? [y/N] ");
return Task.FromResult(Console.ReadLine()?.Trim() == "y");
}
public Task RunDownloadAsync(Func<IProgress<UpdateDownloadProgress>, CancellationToken, Task> download) =>
download(new Progress<UpdateDownloadProgress>(p => Console.Write($"\r{p.Percentage:0}%")), CancellationToken.None);
public Task ShowErrorAsync(UpdateErrorMessage message, Exception? exception)
{
Console.Error.WriteLine($"{message.Caption}: {message.Text}");
return Task.CompletedTask;
}
}
var result = await new UpdateFlow(manager, new ConsolePresenter()).RunAsync();Your own installer
An installer of your own is a small executable on the nUpdate.UpdateInstaller package. InstallerHost does the
work (options, waiting for the application, files, operations, restart, install.log, the windowless mode); you
provide the window by deriving from WindowProgressReporter, which takes care of the threads.
samples/CustomInstaller is a complete WPF example.
// The installer's Program.cs
[STAThread]
static int Main(string[] args) => InstallerHost.Run(args, session => new MyProgressWindow(session));
// In the application: the installer for this platform, in a folder of its own.
manager.InstallerPath = OperatingSystem.IsWindows()
? Path.Combine(AppContext.BaseDirectory, "MyInstaller", "MyInstaller.exe")
: Path.Combine(AppContext.BaseDirectory, "MyInstaller", "MyInstaller");Install without a window, and brand the one you show
// A tray tool or a service: no window, the log tells what happened.
manager.ShowInstallerWindow = false;
// Otherwise the built-in window can carry your icon and colour.
manager.InstallerIcon = Path.Combine(AppContext.BaseDirectory, "Assets", "app-256.png");
manager.InstallerAccentColor = "#2E7D32";Another language
Copy en.json, translate it and register it. English, German (Germany, Austria,
Switzerland), Spanish, Italian and Chinese (simplified) are built in.
manager.TextFiles[new CultureInfo("fr-FR")] = Path.Combine(AppContext.BaseDirectory, "Localization", "fr-FR.json");
manager.Culture = new CultureInfo("fr-FR");Test your update code without network, disk or restarts
var services = new UpdateManagerServices
{
HttpClient = new HttpClient(fakeHandler), // serves nupdate.json and packages from memory
FileSystem = new MockFileSystem(), // System.IO.Abstractions.TestingHelpers
ProcessLauncher = fakeLauncher, // records the installer start instead of running it
ApplicationTerminator = fakeTerminator, // keeps the test process alive
};
using var manager = new UpdateManager(feedUri, publicKey, currentVersion: new UpdateVersion("1.0.0"), services: services);Version 5.0 is a breaking release with new file formats; every document carries an integer format field. Open a
project of nUpdate 3 or 4 in nUpdate Administration 5: it converts the project file, and the migration assistant
publishes the packages for nUpdate 5 next to the old ones, so installations of both versions keep finding their
updates.
Installed copies of your application keep their nUpdate 4 until they update, and nUpdate 4 only reads updates.json.
The migration assistant ends with these steps:
- Check the new feed. The assistant downloads
nupdate.jsonand every package the way a 5.0 client does, checks size, hash, signature and manifest, and asks the statistics API whether it answers. - Build your application with nUpdate 5. Update the packages, create the
UpdateManagerwith the new feed and the PEM public key (Overview > Copy source) and declare the version of that build in the canonical form,[assembly: ApplicationVersion("1.3.0")]. It must be higher than everything inupdates.json: a version the installed copies already have would offer itself as an update again and again. - Ship that build once through the old feed. Publish it with nUpdate Administration 4 under the same version
written the old way (
1.3.0.0), so installed copies update to it and readnupdate.jsonfrom then on. Publish the same version with nUpdate Administration 5 as well, and every later version only there. - Keep the old files until everybody has moved.
updates.json, the version folders andstatistics.phpstay on the server. nUpdate Administration 4 keeps working next to version 5: it keeps its project file,projconf.jsonand package copies, while version 5 usesprojects.json,nupdate.jsonandnupdate-statistics.php. Open the projects of version 5 from nUpdate Administration 5; double-clicking a.nupdprojfile starts whichever version registered the extension, usually version 4. - Retire the nUpdate 4 setup from the Overview tab once nobody downloads from
updates.json. It lists, then deletes the version folders,statistics.phpand finallyupdates.jsonon the server, and the version folders with this project's package copies that nUpdate Administration 4 and the 5.0 pre-releases kept on your computer.
Everything that changed in detail
- Projects are folders: a project is
project.nupdprojplus apackages/folder next to it. Copy the folder to another computer and open the project file there; the export and import feature is gone. The wizard asks where to put the folder (Documents/nUpdate Projects/<name>by default). Secrets (passwords, the private key) are stored in the project file encrypted with a project password (PBKDF2-SHA256 and AES-GCM); the password is remembered per user and machine and asked for once elsewhere. You can also keep the secrets out of the file and enter them when the project is opened. - Projects of earlier versions are converted when you open them: files written by every nUpdate Administration
since 1.0 (the files before 1.0 Beta 2,
1b2,3b2,v3) and by the 5.0 pre-releases (v5). The converted project is saved as<project name>/project.nupdprojnext to the old file (in the old folder when it is named after the project, and inDocuments/nUpdate Projects/<name>when the old file lies in the data folder of nUpdate Administration 4). The old file is never overwritten, so nUpdate Administration 4 can keep using it. The RSA keys are converted from XML to PEM, the same key pair signs on, and the versions are converted to the canonical form. - Published packages are migrated with the migration assistant: the server layout changed from
updates.jsonplus<version>/<id>.ziptonupdate.jsonpluspackages/<version>/<platform>.zip; every package file carries amanifest.jsonwith its platform and operations and is signed with RSA-PSS. Packages for x86, x64 and any architecture becomewin-x86,win-x64andwin, since nUpdate 3 and 4 only ran on Windows. When you open a converted project whose server still has onlyupdates.json, the migration assistant opens. It shows what is on the server and what the migration adds and keeps, lists every package with its new version and with what cannot be carried over (operations without a counterpart such as C# scripts, files outside the known folders, a zip whose nUpdate 4 signature does not match), checks the statistics settings, then repacks, re-signs and uploads the selected packages and writesnupdate.json. Only zips that carry the signatureupdates.jsonnames for them are signed anew. Nothing of nUpdate 3 and 4 is changed, so both versions run side by side (see below). Nothing can be published until the migration ran; the assistant is also on the Overview tab and in the settings. - Client API:
CheckForUpdatesAsync,DownloadAsync,VerifyAsync,StartInstaller()andDeleteDownloads()replaceSearchForUpdatesAsync,DownloadPackagesAsync,ValidatePackagesAsync,InstallPackage()andDeletePackages();AvailableUpdates(a list ofPackageInfofrom theUpdateFeed) replacesPackageConfigurations,FeedUrireplacesUpdateConfigurationFileUri,TotalDownloadSize,DownloadedPackagesandDownloadDirectoryreplaceTotalSize,PackageFilePathsandUpdateDirectory.DefaultAfterInstall(Restart,Close,KeepRunning) replacesHostApplicationOptions, and a package can override it;InstallerArgument(value, when)withArgumentCondition(Succeeded,Failed,Always) replacesUpdateArgument;MinimumStabilityandAcceptedPreReleaseLabelsreplaceIncludeAlphaandIncludeBeta;Culture,TextsandTextFilesreplaceLanguageCulture,LocalizationPropertiesandCultureFilePaths;ReportDownloadsreplacesIncludeCurrentPcIntoStatistics;RolloutConditionsreplacesConditions, withRolloutCondition.NegatedandRolloutConditionMode.Any;Platformand the platform of each package file replaceArchitecture;UpdateManagerServices.Loggerreplaces theStatisticsReportFailedevent;UseDynamicUpdateUriis gone because thepathof a package file in the feed is relative to the feed unless it is absolute;ApplicationVersionAttributereplacesnUpdateVersionAttribute;UpdaterUI.RunAsync()replacesShowUserInterfaceAsync().UpdateVersionlostFullText,SemanticVersion,FromFullTextandDevelopmentBuild;BasicVersionbecameRelease, andToString()is the only string form and the only one that parses: write[assembly: ApplicationVersion("1.2.0-beta.1")]where nUpdate 4 had[assembly: nUpdateVersion("1.2.0.0b1")]. - Installer deployment: the installer is no longer embedded in
nUpdate.dlland no longer needs .NET Framework. Install thenUpdate.UpdateInstaller.UI.Avaloniapackage and ship thenUpdate.Installerfolder, or setInstallerPathto an installer of your own built onnUpdate.UpdateInstaller(custom user interface assemblies andCustomUiAssemblyPathare gone). The installer reads one JSON options file (format2) and themanifest.jsonof every package. Operations are typed (deleteFiles,renameFile,setRegistryValueswith typed registry values,startProcess, which can wait for the process and fail on an error exit code, ...); the C# script operation is gone. - Statistics: a REST API v2 below
nupdate-statistics.php(GET /v2,POST /v2/downloads,GET /v2/projects/{id}/statistics,PUT|DELETE /v2/projects/{id}/versions/{version},DELETE /v2/projects/{id}), bearer-authenticated for everything but the download report, with JSON errors. The script and its configuration (nupdate-statistics.config.php, uploaded without being kept locally) are generated per project and need PHP 7.1 or later with mysqli; the tables (nupdate_version,nupdate_download) are created on first use, and only downloads of versions the administration registered are counted. The names differ from thestatistics.phpand the tables of nUpdate 3 and 4, so both versions report into the same database side by side. There is no compatibility with the earlier endpoints and old statistics are not migrated. - Administration: rewritten with Avalonia for Windows, Linux and macOS. Transfer over SFTP (the default for new
projects), FTPS and FTP is built in (SSH.NET and FluentFTP), the project proxy applies to all of them, and third-party
transfer plugins implement the asynchronous
ITransferProvider; the Starksoft libraries and the IPv4/IPv6 preference are gone. Server certificates and SSH host keys must be trusted explicitly by fingerprint. FTPS cannot resume the TLS session of the control connection on data connections, which vsftpd and ProFTPD require by default; allow fresh sessions on the server (require_ssl_reuse=NO,TLSOptions NoSessionReuseRequired) or use SFTP, as the error message suggests. Renaming a project changes its name only; the folder stays. The data folder (nUpdate Administrationin the roaming application data) only holds the list of known projects (projects.json) and the remembered project passwords. nUpdate Administration 4 uses the same folder: itsprojconf.jsonis read until the new list exists and never changed, and its local package copies are left alone, so both versions can be installed side by side. The remembered passwords are protected with DPAPI on Windows, and on Linux and macOS with a key ring in that folder, so they are only as private as the folder itself. Editing the files of an already created package, the server directory browser and cancelling a running upload are not available in this version. - Timeouts and cleanup: the search timeout grew from 10 to 100 seconds (
HttpTimeout, replacingSearchTimeout); the installer is copied to one fixed temp folder per application and old downloads are removed before a new one. OverridingTerminateApplicationis replaced byUpdateManagerServices.ApplicationTerminator.
BUILDING.md explains how to build, test and check the coverage gate. Every push runs the CI workflow: formatting, the full test suite including the Docker integration tests and the headless UI scenarios on Linux; the NuGet packages with the installers for all seven runtime identifiers; the Windows-only tests and a build of nUpdate Administration on Windows; and a real update with the published installer on Linux, Windows and macOS, including the swap of a signed application bundle.
nUpdate is released under the MIT license.








