diff --git a/docs/Auto Update.md b/docs/Auto Update.md
index 5891c6f39ac..c2816b8c02f 100644
--- a/docs/Auto Update.md	
+++ b/docs/Auto Update.md	
@@ -69,7 +69,7 @@ Emitted when checking if an update has started.
 
 #### Event: `update-available`
 
-* `info` [UpdateInfo](#updateinfo) for generic and github providers. [VersionInfo](#versioninfo) for Bintray provider.
+* `info` [UpdateInfo](#updateinfo) (for generic and github providers) | [VersionInfo](#versioninfo) (for Bintray provider)
 
 Emitted when there is an available update. The update is downloaded automatically if `autoDownload` is `true`.
 
@@ -77,10 +77,10 @@ Emitted when there is an available update. The update is downloaded automaticall
 
 Emitted when there is no available update.
 
-* `info` [UpdateInfo](#updateinfo) for generic and github providers. [VersionInfo](#versioninfo) for Bintray provider.
+* `info` [UpdateInfo](#updateinfo) (for generic and github providers) | [VersionInfo](#versioninfo) (for Bintray provider)
 
 #### Event: `download-progress`
-* `progressObj` - it's object with properties:
+* `progress` ProgressInfo
   * `bytesPerSecond`
   * `percent`
   * `total`
@@ -90,7 +90,7 @@ Emitted on progress. Only supported over Windows build, since `Squirrel.Mac` [do
 
 #### Event: `update-downloaded`
 
-* `info` [UpdateInfo](#updateinfo) for generic and github providers. [VersionInfo](#versioninfo) for Bintray provider.
+* `info` [UpdateInfo](#updateinfo) — for generic and github providers. [VersionInfo](#versioninfo) for Bintray provider.
 
 Emitted when an update has been downloaded.
 
@@ -100,7 +100,7 @@ The `autoUpdater` object has the following methods:
 
 #### `autoUpdater.setFeedURL(options)`
 
-* `options` GenericServerOptions | BintrayOptions | GithubOptions | string — if you want to override configuration in the `app-update.yml`.
+* `options` GenericServerOptions | S3Options | BintrayOptions | GithubOptions | string — if you want to override configuration in the `app-update.yml`.
 
 Sets the `options`. If value is `string`, `GenericServerOptions` will be set with value as `url`.
 
@@ -118,13 +118,13 @@ This is different from the normal quit event sequence.
 
 ### VersionInfo
 
-* `version` The version.
+* `version` string — The version.
 
 ### UpdateInfo
 
 Extends [VersionInfo](#versioninfo).
 
-* `releaseDate` The release date.
-* `releaseName?` The release name.
-* `releaseNotes?` The release notes.
+* `releaseDate` string — The release date.
+* `releaseName` string (optional) — The release name.
+* `releaseNotes` string (optional) — The release notes.
 
diff --git a/docs/Publishing Artifacts.md b/docs/Publishing Artifacts.md
index 1d4a2c6abd6..47abcbb5cd5 100644
--- a/docs/Publishing Artifacts.md	
+++ b/docs/Publishing Artifacts.md	
@@ -118,7 +118,8 @@ Amazon S3 — `https` must be used, so, if you use direct Amazon S3 endpoints, f
 
 | Name | Description
 | --- | ---
-| bucket | <a name="S3Options-bucket"></a>The bucket name.
+| **bucket** | <a name="S3Options-bucket"></a>The bucket name.
+| path | <a name="S3Options-path"></a>The directory path. Defaults to `/`.
 | channel | <a name="S3Options-channel"></a>The channel. Defaults to `latest`.
 | acl | <a name="S3Options-acl"></a>The ACL. Defaults to `public-read`.
 | storageClass | <a name="S3Options-storageClass"></a>The type of storage to use for the object. One of `STANDARD`, `REDUCED_REDUNDANCY`, `STANDARD_IA`. Defaults to `STANDARD`.
diff --git a/packages/electron-builder-http/src/httpExecutor.ts b/packages/electron-builder-http/src/httpExecutor.ts
index 25567127abd..d68cb749b39 100644
--- a/packages/electron-builder-http/src/httpExecutor.ts
+++ b/packages/electron-builder-http/src/httpExecutor.ts
@@ -4,7 +4,7 @@ import { createWriteStream } from "fs-extra-p"
 import { RequestOptions } from "http"
 import { parse as parseUrl } from "url"
 import _debug from "debug"
-import { ProgressCallbackTransform } from "./ProgressCallbackTransform"
+import { ProgressCallbackTransform, ProgressInfo } from "./ProgressCallbackTransform"
 import { safeLoad } from "js-yaml"
 import { EventEmitter } from "events"
 import { Socket } from "net"
@@ -30,7 +30,7 @@ export interface DownloadOptions {
 
   readonly cancellationToken: CancellationToken
 
-  onProgress?(progress: any): void
+  onProgress?(progress: ProgressInfo): void
 }
 
 export class HttpExecutorHolder {
diff --git a/packages/electron-builder-http/src/publishOptions.ts b/packages/electron-builder-http/src/publishOptions.ts
index 6c37d67f116..0d4a7d830bb 100644
--- a/packages/electron-builder-http/src/publishOptions.ts
+++ b/packages/electron-builder-http/src/publishOptions.ts
@@ -53,7 +53,12 @@ export interface S3Options extends PublishConfiguration {
   /*
   The bucket name.
    */
-  bucket?: string
+  bucket: string
+
+  /*
+  The directory path. Defaults to `/`.
+   */
+  path?: string | null
 
   /**
   The channel. Defaults to `latest`.
@@ -73,6 +78,14 @@ export interface S3Options extends PublishConfiguration {
   secret?: string | null
 }
 
+export function s3Url(options: S3Options) {
+  let url = `https://s3.amazonaws.com/${options.bucket}`
+  if (options.path != null) {
+    url += `/${options.path}`
+  }
+  return url
+}
+
 export interface VersionInfo {
   readonly version: string
 }
diff --git a/packages/electron-builder/src/options/winOptions.ts b/packages/electron-builder/src/options/winOptions.ts
index 68f9eb3fb9e..e56cc105f08 100644
--- a/packages/electron-builder/src/options/winOptions.ts
+++ b/packages/electron-builder/src/options/winOptions.ts
@@ -154,6 +154,15 @@ export interface NsisOptions {
   readonly artifactName?: string | null
 }
 
+export interface NsisWebOptions extends NsisOptions {
+  /*
+  The application package download URL. Optional — by default computed using publish configuration.
+
+  URL like `https://example.com/download/latest` allows web installer to be version independent (installer will download latest application package).
+   */
+  readonly appPackageUrl?: string | null
+}
+
 /*
  ### `squirrelWindows`
 
diff --git a/packages/electron-builder/src/publish/PublishManager.ts b/packages/electron-builder/src/publish/PublishManager.ts
index 0a2c1ac3f3e..99af515a324 100644
--- a/packages/electron-builder/src/publish/PublishManager.ts
+++ b/packages/electron-builder/src/publish/PublishManager.ts
@@ -1,7 +1,7 @@
 import BluebirdPromise from "bluebird-lst-c"
 import { createHash } from "crypto"
 import { Arch, Platform } from "electron-builder-core"
-import { GenericServerOptions, GithubOptions, PublishConfiguration, S3Options, UpdateInfo, VersionInfo } from "electron-builder-http/out/publishOptions"
+import { GenericServerOptions, GithubOptions, PublishConfiguration, S3Options, UpdateInfo, VersionInfo, s3Url } from "electron-builder-http/out/publishOptions"
 import { asArray, debug, isEmptyOrSpaces } from "electron-builder-util"
 import { log } from "electron-builder-util/out/log"
 import { throwError } from "electron-builder-util/out/promise"
@@ -172,7 +172,7 @@ export class PublishManager implements PublishContext {
   }
 }
 
-async function getPublishConfigsForUpdateInfo(packager: PlatformPackager<any>, publishConfigs: Array<PublishConfiguration> | null): Promise<Array<PublishConfiguration> | null> {
+export async function getPublishConfigsForUpdateInfo(packager: PlatformPackager<any>, publishConfigs: Array<PublishConfiguration> | null): Promise<Array<PublishConfiguration> | null> {
   if (publishConfigs === null) {
     return null
   }
@@ -281,19 +281,30 @@ export function createPublisher(context: PublishContext, version: string, publis
   return null
 }
 
-function computeDownloadUrl(publishConfig: PublishConfiguration, fileName: string, version: string, macros: Macros) {
+export function computeDownloadUrl(publishConfig: PublishConfiguration, fileName: string | null, version: string, macros: Macros) {
   if (publishConfig.provider === "generic") {
-    const baseUrl = url.parse(expandPattern((<GenericServerOptions>publishConfig).url, macros))
+    const baseUrlString = expandPattern((<GenericServerOptions>publishConfig).url, macros)
+    if (fileName == null) {
+      return baseUrlString
+    }
+
+    const baseUrl = url.parse(baseUrlString)
     return url.format(Object.assign({}, baseUrl, {pathname: path.posix.resolve(baseUrl.pathname || "/", encodeURI(fileName))}))
   }
-  else if (publishConfig.provider === "s3") {
-    const bucket = (<S3Options>publishConfig).bucket
-    return `https://s3.amazonaws.com/${bucket}/${fileName}`
+
+  let baseUrl
+  if (publishConfig.provider === "s3") {
+    baseUrl = s3Url((<S3Options>publishConfig))
   }
   else {
     const gh = <GithubOptions>publishConfig
-    return `https://github.com${`/${gh.owner}/${gh.repo}/releases`}/download/v${version}/${encodeURI(fileName)}`
+    baseUrl = `https://github.com${`/${gh.owner}/${gh.repo}/releases`}/download/v${version}`
+  }
+
+  if (fileName == null) {
+    return baseUrl
   }
+  return `${baseUrl}/${encodeURI(fileName)}`
 }
 
 function expandPattern(pattern: string, macros: Macros): string {
diff --git a/packages/electron-builder/src/targets/nsis.ts b/packages/electron-builder/src/targets/nsis.ts
index 1ba2b13333f..9626ff0b2af 100644
--- a/packages/electron-builder/src/targets/nsis.ts
+++ b/packages/electron-builder/src/targets/nsis.ts
@@ -8,10 +8,11 @@ import { normalizeExt } from "../platformPackager"
 import { archive } from "./archive"
 import { subTask, log, warn } from "electron-builder-util/out/log"
 import { unlink, readFile } from "fs-extra-p"
-import { NsisOptions } from "../options/winOptions"
-import { Target, Arch } from "electron-builder-core"
+import { NsisOptions, NsisWebOptions } from "../options/winOptions"
+import { Target, Arch, Platform } from "electron-builder-core"
 import sanitizeFileName from "sanitize-filename"
 import { copyFile } from "electron-builder-util/out/fs"
+import { computeDownloadUrl, getPublishConfigs, getPublishConfigsForUpdateInfo } from "../publish/PublishManager"
 
 const NSIS_VERSION = "3.0.1.5"
 //noinspection SpellCheckingInspection
@@ -25,14 +26,20 @@ const nsisPathPromise = getBinFromBintray("nsis", NSIS_VERSION, NSIS_SHA2)
 const USE_NSIS_BUILT_IN_COMPRESSOR = false
 
 export default class NsisTarget extends Target {
-  private readonly options: NsisOptions = this.packager.config.nsis || Object.create(null)
+  private readonly options: NsisOptions
 
   private archs: Map<Arch, string> = new Map()
 
   private readonly nsisTemplatesDir = path.join(__dirname, "..", "..", "templates", "nsis")
 
-  constructor(private packager: WinPackager, private outDir: string) {
-    super("nsis")
+  constructor(private packager: WinPackager, private outDir: string, targetName: string) {
+    super(targetName)
+
+    let options = this.packager.config.nsis || Object.create(null)
+    if (targetName !== "nsis") {
+      options = Object.assign(options, (<any>this.packager.config)[targetName])
+    }
+    this.options = options
 
     const deps = packager.info.metadata.dependencies
     if (deps != null && deps["electron-squirrel-startup"] != null) {
@@ -106,10 +113,118 @@ export default class NsisTarget extends Target {
       await BluebirdPromise.map(this.archs.keys(), async arch => {
         const file = await this.doBuild(this.archs.get(arch)!, arch)
         defines[arch === Arch.x64 ? "APP_64" : "APP_32"] = file
-        filesToDelete.push(file)
+        defines[(arch === Arch.x64 ? "APP_64" : "APP_32") + "_NAME"] = path.basename(file)
+
+        if (this.isWebInstaller) {
+          packager.dispatchArtifactCreated(file, this)
+        }
+        else {
+          filesToDelete.push(file)
+        }
       })
     }
 
+    await this.configureDefines(oneClick, defines)
+
+    if (this.isWebInstaller) {
+      let appPackageUrl = (<NsisWebOptions>options).appPackageUrl
+      if (appPackageUrl == null) {
+        const publishConfigs = await getPublishConfigsForUpdateInfo(packager, await getPublishConfigs(packager, this.options, false))
+        if (publishConfigs == null || publishConfigs.length === 0) {
+          throw new Error("Cannot compute app package download URL")
+        }
+
+        computeDownloadUrl(publishConfigs[0], null, packager.appInfo.version, {
+          os: Platform.WINDOWS.buildConfigurationKey,
+          arch: Arch[Arch.x64]
+        })
+
+        defines.APP_PACKAGE_URL_IS_INCOMLETE = null
+      }
+
+      defines.APP_PACKAGE_URL = appPackageUrl
+    }
+
+    const commands: any = {
+      OutFile: `"${installerPath}"`,
+      VIProductVersion: appInfo.versionInWeirdWindowsForm,
+      VIAddVersionKey: this.computeVersionKey(),
+      Unicode: true,
+    }
+
+    if (packager.config.compression === "store") {
+      commands.SetCompress = "off"
+    }
+    else {
+      commands.SetCompressor = "lzma"
+      if (!this.isWebInstaller) {
+        defines.COMPRESS = "auto"
+      }
+    }
+
+    debug(defines)
+    debug(commands)
+
+    if (packager.packagerOptions.effectiveOptionComputed != null && await packager.packagerOptions.effectiveOptionComputed([defines, commands])) {
+      return
+    }
+
+    await subTask(`Executing makensis — installer`, this.executeMakensis(defines, commands, true, await this.computeScript(defines, commands, installerPath)))
+    await packager.sign(installerPath)
+
+    packager.dispatchArtifactCreated(installerPath, this, `${packager.appInfo.name}-Setup-${version}.exe`)
+  }
+
+  private get isWebInstaller(): boolean {
+    return this.name === "nsis-web"
+  }
+
+  private async computeScript(defines: any, commands: any, installerPath: string) {
+    const packager = this.packager
+    const customScriptPath = await packager.getResource(this.options.script, "installer.nsi")
+    const script = await readFile(customScriptPath || path.join(this.nsisTemplatesDir, "installer.nsi"), "utf8")
+
+    if (customScriptPath != null) {
+      log("Custom NSIS script is used - uninstaller is not signed by electron-builder")
+      return script
+    }
+
+    const uninstallerPath = await
+    packager.getTempFile("uninstaller.exe")
+    const isWin = process.platform === "win32"
+    defines.BUILD_UNINSTALLER = null
+    defines.UNINSTALLER_OUT_FILE = isWin ? uninstallerPath : path.win32.join("Z:", uninstallerPath)
+    await subTask(`Executing makensis — uninstaller`, this.executeMakensis(defines, commands, false, script))
+    await exec(isWin ? installerPath : "wine", isWin ? [] : [installerPath])
+    await packager.sign(uninstallerPath)
+
+    delete defines.BUILD_UNINSTALLER
+    // platform-specific path, not wine
+    defines.UNINSTALLER_OUT_FILE = uninstallerPath
+    return script
+  }
+
+  private computeVersionKey() {
+    // Error: invalid VIProductVersion format, should be X.X.X.X
+    // so, we must strip beta
+    const localeId = this.options.language || "1033"
+    const appInfo = this.packager.appInfo
+    const versionKey = [
+      `/LANG=${localeId} ProductName "${appInfo.productName}"`,
+      `/LANG=${localeId} ProductVersion "${appInfo.version}"`,
+      `/LANG=${localeId} CompanyName "${appInfo.companyName}"`,
+      `/LANG=${localeId} LegalCopyright "${appInfo.copyright}"`,
+      `/LANG=${localeId} FileDescription "${appInfo.description}"`,
+      `/LANG=${localeId} FileVersion "${appInfo.buildVersion}"`,
+    ]
+    use(this.packager.platformSpecificBuildOptions.legalTrademarks, it => versionKey.push(`/LANG=${localeId} LegalTrademarks "${it}"`))
+    return versionKey
+  }
+
+  private async configureDefines(oneClick: boolean, defines: any) {
+    const packager = this.packager
+    const options = this.options
+
     const installerHeader = oneClick ? null : await packager.getResource(options.installerHeader, "installerHeader.bmp")
     if (installerHeader != null) {
       defines.MUI_HEADERIMAGE = null
@@ -146,87 +261,26 @@ export default class NsisTarget extends Target {
       defines.allowToChangeInstallationDirectory = null
     }
 
-    // Error: invalid VIProductVersion format, should be X.X.X.X
-    // so, we must strip beta
-    const localeId = options.language || "1033"
-    const versionKey = [
-      `/LANG=${localeId} ProductName "${appInfo.productName}"`,
-      `/LANG=${localeId} ProductVersion "${appInfo.version}"`,
-      `/LANG=${localeId} CompanyName "${appInfo.companyName}"`,
-      `/LANG=${localeId} LegalCopyright "${appInfo.copyright}"`,
-      `/LANG=${localeId} FileDescription "${appInfo.description}"`,
-      `/LANG=${localeId} FileVersion "${appInfo.buildVersion}"`,
-    ]
-    use(packager.platformSpecificBuildOptions.legalTrademarks, it => versionKey.push(`/LANG=${localeId} LegalTrademarks "${it}"`))
-
-    const commands: any = {
-      OutFile: `"${installerPath}"`,
-      VIProductVersion: appInfo.versionInWeirdWindowsForm,
-      VIAddVersionKey: versionKey,
-      Unicode: true,
-    }
-
-    if (packager.config.compression === "store") {
-      commands.SetCompress = "off"
-    }
-    else {
-      commands.SetCompressor = "lzma"
-      defines.COMPRESS = "auto"
-    }
+    if (!this.isWebInstaller && defines.APP_BUILD_DIR == null) {
+      if (options.useZip) {
+        defines.ZIP_COMPRESSION = null
+      }
 
-    if (this.options.useZip) {
-      defines.ZIP_COMPRESSION = null
+      defines.COMPRESSION_METHOD = options.useZip ? "zip" : "7z"
     }
 
-    defines.COMPRESSION_METHOD = this.options.useZip ? "zip" : "7z"
-
     if (oneClick) {
       defines.ONE_CLICK = null
     }
 
     if (options.menuCategory != null) {
-      const menu = sanitizeFileName(options.menuCategory === true ? appInfo.companyName : <string>options.menuCategory)
+      const menu = sanitizeFileName(options.menuCategory === true ? packager.appInfo.companyName : <string>options.menuCategory)
       if (!isEmptyOrSpaces(menu)) {
         defines.MENU_FILENAME = menu
       }
     }
 
-    debug(defines)
-    debug(commands)
-
-    if (packager.packagerOptions.effectiveOptionComputed != null && await packager.packagerOptions.effectiveOptionComputed([defines, commands])) {
-      return
-    }
-
-    const licenseFile = await packager.getResource(options.license, "license.rtf", "license.txt")
-    if (licenseFile != null) {
-      defines.LICENSE_FILE = licenseFile
-    }
-
-    const customScriptPath = await packager.getResource(options.script, "installer.nsi")
-    const script = await readFile(customScriptPath || path.join(this.nsisTemplatesDir, "installer.nsi"), "utf8")
-
-    if (customScriptPath == null) {
-      const uninstallerPath = await packager.getTempFile("uninstaller.exe")
-      const isWin = process.platform === "win32"
-      defines.BUILD_UNINSTALLER = null
-      defines.UNINSTALLER_OUT_FILE = isWin ? uninstallerPath : path.win32.join("Z:", uninstallerPath)
-      await subTask(`Executing makensis — uninstaller`, this.executeMakensis(defines, commands, false, script))
-      await exec(isWin ? installerPath : "wine", isWin ? [] : [installerPath])
-      await packager.sign(uninstallerPath)
-
-      delete defines.BUILD_UNINSTALLER
-      // platform-specific path, not wine
-      defines.UNINSTALLER_OUT_FILE = uninstallerPath
-    }
-    else {
-      log("Custom NSIS script is used - uninstaller is not signed by electron-builder")
-    }
-
-    await subTask(`Executing makensis — installer`, this.executeMakensis(defines, commands, true, script))
-    await packager.sign(installerPath)
-
-    packager.dispatchArtifactCreated(installerPath, this, `${packager.appInfo.name}-Setup-${version}.exe`)
+    use(await packager.getResource(options.license, "license.rtf", "license.txt"), it => defines.LICENSE_FILE = it)
   }
 
   private async executeMakensis(defines: any, commands: any, isInstaller: boolean, originalScript: string) {
diff --git a/packages/electron-builder/src/winPackager.ts b/packages/electron-builder/src/winPackager.ts
index 9da367359d9..b10530cac59 100644
--- a/packages/electron-builder/src/winPackager.ts
+++ b/packages/electron-builder/src/winPackager.ts
@@ -78,6 +78,7 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
       const targetClass: typeof NsisTarget | typeof AppXTarget | null = (() => {
         switch (name) {
           case "nsis":
+          case "nsis-web":
             return require("./targets/nsis").default
 
           case "squirrel":
@@ -85,7 +86,7 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
               return require("electron-builder-squirrel-windows").default
             }
             catch (e) {
-              throw new Error(`Since electron-builder 11, module electron-builder-squirrel-windows must be installed in addition to build Squirrel.Windows: ${e.stack || e}`)
+              throw new Error(`Module electron-builder-squirrel-windows must be installed in addition to build Squirrel.Windows: ${e.stack || e}`)
             }
 
           case "appx":
@@ -96,7 +97,7 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
         }
       })()
 
-      mapper(name, outDir => targetClass === null ? createCommonTarget(name, outDir, this) : new targetClass(this, outDir))
+      mapper(name, outDir => targetClass === null ? createCommonTarget(name, outDir, this) : new (<any>targetClass)(this, outDir, name))
     }
   }
 
@@ -129,8 +130,7 @@ export class WinPackager extends PlatformPackager<WinBuildOptions> {
   async sign(file: string) {
     const cscInfo = await this.cscInfo
     if (cscInfo == null) {
-      const forceCodeSigningPlatform = this.platformSpecificBuildOptions.forceCodeSigning
-      if (forceCodeSigningPlatform == null ? this.config.forceCodeSigning : forceCodeSigningPlatform) {
+      if (this.forceCodeSigning) {
         throw new Error(`App is not signed and "forceCodeSigning" is set to true, please ensure that code signing configuration is correct, please see https://github.com/electron-userland/electron-builder/wiki/Code-Signing`)
       }
 
diff --git a/packages/electron-builder/templates/nsis/installSection.nsh b/packages/electron-builder/templates/nsis/installSection.nsh
index 61a1752f001..81e76a7d2b4 100644
--- a/packages/electron-builder/templates/nsis/installSection.nsh
+++ b/packages/electron-builder/templates/nsis/installSection.nsh
@@ -111,41 +111,74 @@ SetOutPath $INSTDIR
 !ifdef APP_BUILD_DIR
   File /r "${APP_BUILD_DIR}/*.*"
 !else
-  !ifdef COMPRESS
-    SetCompress off
-  !endif
+  !ifdef APP_PACKAGE_URL
+    StrCpy $0 "${APP_PACKAGE_URL}"
+    !ifdef APP_PACKAGE_URL_IS_INCOMLETE
+      !ifdef APP_64_NAME
+        !ifdef APP_32_NAME
+          ${if} ${RunningX64}
+            StrCpy $0 "$0/${APP_64_NAME}"
+          ${else}
+            StrCpy $0 "$0/${APP_32_NAME}"
+          ${endif}
+        !else
+          StrCpy $0 "$0/${APP_64_NAME}"
+        !endif
+      !else
+        StrCpy $0 "$0/${APP_32_NAME}"
+      !endif
+    !endif
 
-  !ifdef APP_32
-    File /oname=$PLUGINSDIR\app-32.${COMPRESSION_METHOD} "${APP_32}"
-  !endif
-  !ifdef APP_64
-    File /oname=$PLUGINSDIR\app-64.${COMPRESSION_METHOD} "${APP_64}"
-  !endif
+    download:
+    inetc::get /RESUME "$0" "$PLUGINSDIR\package.7z"
+    pop $0
+    ${if} $0 == "Cancelled"
+      quit
+    ${elseif} $0 != "OK"
+      messagebox MB_RETRYCANCEL|MB_ICONEXCLAMATION "Unable to download application package.$\r$\n$\r$\nPlease check you Internet connection and retry." IDRETRY download
+      quit
+    ${endif}
 
-  !ifdef COMPRESS
-    SetCompress "${COMPRESS}"
-  !endif
+    Nsis7z::Extract "$PLUGINSDIR\package.7z"
+  !else
+    !ifdef COMPRESS
+      SetCompress off
+    !endif
 
-  !ifdef APP_64
-    ${if} ${RunningX64}
-      !ifdef ZIP_COMPRESSION
-        nsisunz::Unzip "$PLUGINSDIR\app-64.zip" "$INSTDIR"
-      !else
-        Nsis7z::Extract "$PLUGINSDIR\app-64.7z"
-      !endif
-    ${else}
+    !ifdef APP_32
+      File /oname=$PLUGINSDIR\app-32.${COMPRESSION_METHOD} "${APP_32}"
+    !endif
+    !ifdef APP_64
+      File /oname=$PLUGINSDIR\app-64.${COMPRESSION_METHOD} "${APP_64}"
+    !endif
+
+    !ifdef COMPRESS
+      SetCompress "${COMPRESS}"
+    !endif
+
+    !ifdef APP_64
+      ${if} ${RunningX64}
+        !ifdef ZIP_COMPRESSION
+          nsisunz::Unzip "$PLUGINSDIR\app-64.zip" "$INSTDIR"
+        !else
+          Nsis7z::Extract "$PLUGINSDIR\app-64.7z"
+        !endif
+      ${else}
+        !ifdef ZIP_COMPRESSION
+          nsisunz::Unzip "$PLUGINSDIR\app-32.zip" "$INSTDIR"
+        !else
+          Nsis7z::Extract "$PLUGINSDIR\app-32.7z"
+        !endif
+      ${endif}
+    !else
       !ifdef ZIP_COMPRESSION
         nsisunz::Unzip "$PLUGINSDIR\app-32.zip" "$INSTDIR"
       !else
         Nsis7z::Extract "$PLUGINSDIR\app-32.7z"
       !endif
-    ${endif}
-  !else
-    !ifdef ZIP_COMPRESSION
-      nsisunz::Unzip "$PLUGINSDIR\app-32.zip" "$INSTDIR"
-    !else
-      Nsis7z::Extract "$PLUGINSDIR\app-32.7z"
     !endif
+  !else
+    inetc::get
   !endif
 !endif
 
diff --git a/packages/electron-updater/src/AppUpdater.ts b/packages/electron-updater/src/AppUpdater.ts
index d04fea107b4..1e565ed3054 100644
--- a/packages/electron-updater/src/AppUpdater.ts
+++ b/packages/electron-updater/src/AppUpdater.ts
@@ -5,7 +5,7 @@ import { RequestHeaders, executorHolder } from "electron-builder-http"
 import { Provider, UpdateCheckResult, FileInfo, UpdaterSignal } from "./api"
 import { BintrayProvider } from "./BintrayProvider"
 import BluebirdPromise from "bluebird-lst-c"
-import { BintrayOptions, PublishConfiguration, GithubOptions, S3Options, GenericServerOptions, VersionInfo } from "electron-builder-http/out/publishOptions"
+import { BintrayOptions, PublishConfiguration, GithubOptions, S3Options, GenericServerOptions, VersionInfo, s3Url } from "electron-builder-http/out/publishOptions"
 import { readFile } from "fs-extra-p"
 import { safeLoad } from "js-yaml"
 import { GenericProvider } from "./GenericProvider"
@@ -239,7 +239,7 @@ export abstract class AppUpdater extends EventEmitter {
   }
 }
 
-function createClient(data: string | PublishConfiguration | BintrayOptions | GithubOptions | S3Options) {
+function createClient(data: string | PublishConfiguration) {
   if (typeof data === "string") {
     throw new Error("Please pass PublishConfiguration object")
   }
@@ -248,15 +248,22 @@ function createClient(data: string | PublishConfiguration | BintrayOptions | Git
   switch (provider) {
     case "github":
       return new GitHubProvider(<GithubOptions>data)
-    case "s3":
-      return new GenericProvider(<GenericServerOptions>{
-        url: `https://s3.amazonaws.com/${(<S3Options>data).bucket || ""}`,
-        channel: (<S3Options>data).channel || ""
+
+    case "s3": {
+      const s3 = <S3Options>data
+      return new GenericProvider({
+        provider: "generic",
+        url: s3Url(s3),
+        channel: s3.channel || ""
       })
+    }
+
     case "generic":
       return new GenericProvider(<GenericServerOptions>data)
+
     case "bintray":
       return new BintrayProvider(<BintrayOptions>data)
+
     default:
       throw new Error(`Unsupported provider: ${provider}`)
   }
diff --git a/test/out/windows/__snapshots__/nsisTest.js.snap b/test/out/windows/__snapshots__/nsisTest.js.snap
index d8ef9b9b6f8..3105371740b 100644
--- a/test/out/windows/__snapshots__/nsisTest.js.snap
+++ b/test/out/windows/__snapshots__/nsisTest.js.snap
@@ -5,6 +5,12 @@ Array [
 `;
 
 exports[`test allowToChangeInstallationDirectory 2`] = `
+Array [
+  "test-custom-inst-dir-Setup-1.1.0.exe",
+]
+`;
+
+exports[`test allowToChangeInstallationDirectory 3`] = `
 Object {
   "owner": "foo",
   "provider": "github",
@@ -12,7 +18,7 @@ Object {
 }
 `;
 
-exports[`test allowToChangeInstallationDirectory 3`] = `
+exports[`test allowToChangeInstallationDirectory 4`] = `
 Object {
   "githubArtifactName": "test-custom-inst-dir-Setup-1.1.0.exe",
   "path": "Test Custom Installation Dir Setup 1.1.0.exe",
@@ -26,18 +32,36 @@ Array [
 ]
 `;
 
+exports[`test custom include 2`] = `
+Array [
+  "TestApp-Setup-1.1.0.exe",
+]
+`;
+
 exports[`test custom script 1`] = `
 Array [
   "Test App ßW Setup 1.1.0.exe",
 ]
 `;
 
+exports[`test custom script 2`] = `
+Array [
+  "TestApp-Setup-1.1.0.exe",
+]
+`;
+
 exports[`test menuCategory 1`] = `
 Array [
   "Test Menu Category CustomName 1.1.0.exe",
 ]
 `;
 
+exports[`test menuCategory 2`] = `
+Array [
+  "test-menu-category-Setup-1.1.0.exe",
+]
+`;
+
 exports[`test one-click 1`] = `
 Array [
   "Test App ßW Setup 1.1.0.exe",
@@ -45,6 +69,12 @@ Array [
 `;
 
 exports[`test one-click 2`] = `
+Array [
+  "TestApp-Setup-1.1.0.exe",
+]
+`;
+
+exports[`test one-click 3`] = `
 Object {
   "owner": "actperepo",
   "package": "TestApp",
@@ -60,16 +90,35 @@ Array [
 `;
 
 exports[`test perMachine, no run after finish 2`] = `
+Array [
+  "TestApp-Setup-1.1.0.exe",
+]
+`;
+
+exports[`test perMachine, no run after finish 3`] = `
 Object {
   "provider": "generic",
   "url": "https://develar.s3.amazonaws.com/test/win/x64",
 }
 `;
 
-exports[`test perMachine, no run after finish 3`] = `
+exports[`test perMachine, no run after finish 4`] = `
 Object {
   "githubArtifactName": "TestApp-Setup-1.1.0.exe",
   "path": "TestApp Setup 1.1.0.exe",
   "version": "1.1.0",
 }
 `;
+
+exports[`test web installer 1`] = `
+Array [
+  "Test App ßW Setup 1.1.0.exe",
+  "TestApp-1.1.0-x64.nsis.7z",
+]
+`;
+
+exports[`test web installer 2`] = `
+Array [
+  "TestApp-Setup-1.1.0.exe",
+]
+`;
diff --git a/test/src/mac/macArchiveTest.ts b/test/src/mac/macArchiveTest.ts
index 0a719183688..f9a0c9a3a0c 100644
--- a/test/src/mac/macArchiveTest.ts
+++ b/test/src/mac/macArchiveTest.ts
@@ -15,10 +15,9 @@ test("only zip", createMacTargetTest(["zip"]));
 test("tar.gz", createMacTargetTest(["tar.gz"]))
 
 const it = process.env.CSC_KEY_PASSWORD == null ? test.skip : test.ifMac
-
 it("pkg", createMacTargetTest(["pkg"]))
 
-it.ifMac("pkg scripts", app({
+test.ifMac("pkg scripts", app({
   targets: Platform.MAC.createTarget("pkg"),
 }, {
   useTempDir: true,
diff --git a/test/src/windows/nsisTest.ts b/test/src/windows/nsisTest.ts
index d5e1346b333..016f45c5801 100644
--- a/test/src/windows/nsisTest.ts
+++ b/test/src/windows/nsisTest.ts
@@ -225,4 +225,15 @@ test.ifDevOrLinuxCi("file associations only perMachine", appThrows(/Please set p
       }
     ],
   },
+}))
+
+test.ifNotCiMac("web installer", app({
+  targets: Platform.WINDOWS.createTarget(["nsis-web"], Arch.x64),
+  config: {
+    publish: {
+      provider: "s3",
+      bucket: "develar",
+      path: "test",
+    }
+  }
 }))
\ No newline at end of file