Build your own install manager
Let customers sign in once and install, activate and update everything they own from you, in an app of your own, with the same Moonbase APIs Moonbase Hub uses.
Written By Tobias Lønnerød Madsen
An install manager is a small app your customers keep on their computer. They sign in once with their customer account, see everything they own from you, and install, activate and update it from one place. Moonbase Hub is one we build and sign for you (see Moonbase Hub: your own install manager for Mac and Windows). If you'd rather build your own, or add the same thing to an app you already ship, this guide shows how Moonbase Hub works and which Moonbase APIs it calls.
It's written for developers, and assumes your products already use a Moonbase licensing SDK and have releases in Moonbase.
How it works
Your install manager is a product in Moonbase too. The customer signs in to it in their browser, the same way they activate any of your products, and the app gets a license token for them.
With that token, the app asks Moonbase what the customer owns, along with each product's downloads.
It downloads the installer for the customer's computer and runs it.
It checks for the files listed in the product's download manifest to tell whether the product is installed, and which version.
It activates the product on the computer and saves the license file where your product looks for it, so nobody copies a license key.
Every call goes to your Moonbase address, https://<account-id>.moonbase.sh, as the signed-in customer. Your app never needs an API key.
Before you start
A product for your install manager. Create a product for the app itself, such as
acme-hub, and set it up so every customer can sign in to it:In its Licensing section, choose Actions > Enable auto-provisioning and turn it on, so customers get a license for it by signing in, without buying anything.
Under Basic Details, set Customer visibility to Always, so it's in every customer's account, ready to download.
Under Releases > Actions > Configure security, choose Open access to download, so customers can download your app before they've signed in.
Your Account ID and public key. Both are under Account settings > Developer > Account details, and your licensing SDK needs them.
A download manifest for each product. Set one per platform on the product's page, under Releases > Actions > Default manifests. It lists the files your installer places, and optionally where your product keeps its license. Your install manager reads it in steps 4 and 5.
Step 1: Sign the customer in
Use your licensing SDK's browser activation for your install manager's product. It opens a sign-in page in the customer's browser, and once they've signed in, gives your app a license for the product. That license is the customer's sign-in.
import { FileLicenseStore, MoonbaseLicensing } from '@moonbase.sh/licensing'
import { shell } from 'electron'
export const ENDPOINT = 'https://acme.moonbase.sh'
export const PUBLIC_KEY = '-----BEGIN RSA PUBLIC KEY-----...'
const hub = new MoonbaseLicensing({
productId: 'acme-hub',
endpoint: ENDPOINT,
publicKey: PUBLIC_KEY,
licenseStore: new FileLicenseStore(),
})
export async function signIn() {
const request = await hub.client.requestActivation()
await shell.openExternal(request.browser)
// Ask every few seconds until the customer has signed in.
let license = null
while (!license) {
await new Promise(resolve => setTimeout(resolve, 3000))
license = await hub.client.getRequestedActivation(request)
}
await hub.store.storeLocalLicense(license)
return license
}using System.Diagnostics;
using Moonbase.Net;
using Moonbase.Net.Storage;
const string Endpoint = "https://acme.moonbase.sh";
const string PublicKey = "-----BEGIN RSA PUBLIC KEY-----...";
var hub = LicensingFactory.CreateInstance(opts =>
{
opts.Endpoint = new Uri(Endpoint);
opts.ProductId = "acme-hub";
opts.AccountId = "acme";
opts.PublicKey = PublicKey;
opts.LicenseStore = new FileLicenseStore();
});
async Task<License> SignIn()
{
var request = await hub.Client.RequestActivation();
Process.Start(new ProcessStartInfo(request.Browser.ToString()) { UseShellExecute = true });
// Ask every few seconds until the customer has signed in.
License? license = null;
while (license == null)
{
await Task.Delay(TimeSpan.FromSeconds(3));
license = await hub.Client.GetRequestedActivation(request);
}
await hub.Store.StoreLocalLicense(license);
return license;
}Store the license like any other, and validate it when your app starts, as your SDK's documentation describes.
From then on, send the license's token with every call to Moonbase, in the Authorization header with the LicenseToken scheme:
export async function moonbase(url, options = {}) {
const license = await hub.store.loadLocalLicense()
return fetch(new URL(url, ENDPOINT), {
...options,
headers: { ...options.headers, Authorization: `LicenseToken ${license.token}` },
})
}using System.Net.Http.Headers;
var license = await hub.Store.LoadLocalLicense();
var moonbase = new HttpClient { BaseAddress = new Uri(Endpoint) };
moonbase.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("LicenseToken", license!.Token);When Moonbase answers 401, the token no longer works: sign the customer out and ask them to sign in again.
Step 2: List what the customer owns
GET https://acme.moonbase.sh/api/customer/inventory/products?includeManifests=true&pageSize=50Moonbase answers with the products the customer has, each with its current version and that release's downloads:
{
"items": [
{
"id": "helios-synth",
"name": "Helios Synth",
"iconUrl": "https://...",
"currentVersion": "1.4.2",
"downloads": [
{
"name": "Helios Synth 1.4.2.pkg",
"platform": "Mac",
"size": 104857600,
"path": "https://acme.moonbase.sh/api/customer/inventory/products/helios-synth/download/1.4.2/...",
"manifest": {
"files": [
{ "name": "Helios Synth.vst3", "path": "/Library/Audio/Plug-Ins/VST3" },
{ "name": "Helios Synth.component", "path": "/Library/Audio/Plug-Ins/Components" }
],
"licensePath": "~/Library/Application Support/Acme Audio/Helios Synth/license.mb",
"licenseFormat": "JWT",
"deviceIdAlgorithm": "MoonbaseV2"
}
}
]
}
],
"hasMore": false,
"next": null
}The list comes a page at a time. While
hasMoreistrue, ask again with thepaginationTokenfromnext.Your install manager is on it. Every product whose customer visibility is Always is listed, so skip your app's own product.
Pick the download for the computer.
platformisMacorWindows, orUniversalfor a file for any system, such as a PDF, that you save rather than install. If you publish separate builds per processor, each also has anarch, such asArm64orX64.manifestis the download's own manifest, or the product's default manifest for that platform. Without either, it's left out.
Step 3: Download and run the installer
Call the download's path, with your token and redirect=false added:
GET https://acme.moonbase.sh/api/customer/inventory/products/helios-synth/download/1.4.2/...?redirect=falseMoonbase answers with { "location": "https://..." }, a link to the file. Download it from there, without your token: only your Moonbase address needs it.
Then run the installer without anyone clicking through it:
Mac:
installer -pkg "Helios Synth.pkg" -target /. It has to run as an administrator, so either ask macOS for the customer's password each time, or install a privileged helper once, as Moonbase Hub does.Windows: run an Inno Setup installer with
/VERYSILENT, an NSIS installer with/S, and an.msiwithmsiexec /i "Helios Synth.msi" /qn.
Wait for the installer to finish before you look for its files (step 4).
Check who signed an installer before you run it as an administrator. Only run installers you signed: check the Team ID with
pkgutil --check-signatureon Mac, and the publisher of the Authenticode signature on Windows. Copy the installer to a folder only administrators can write to, then check it and run it from there, so nothing can swap it in between.
Step 4: Tell whether it's installed and up to date
The manifest's files are what your installer places on the computer, each a folder (path) and the name inside it (name).
Installed when every listed file is there. When only some are, offer to install it again.
The installed version is the version of a listed file:
CFBundleShortVersionStringin a bundle'sContents/Info.pliston Mac, and the file version on Windows.An update is ready when that version is lower than the product's
currentVersion. Install it the same way, from step 3.
So give your binaries the same version number as the release, such as 1.4.2, and list only what each platform's installer actually places.
Step 5: Activate the product
When the manifest has a licensePath, activate the product as soon as it's installed:
Make a device token for the product with your licensing SDK, set up with that product's ID. It's the same device token an offline activation uses.
Send it to Moonbase as a JSON string, with your token. Moonbase activates the customer's license for that product on this computer, and answers with a
Locationheader.Download the license file from that
Location, and save it atlicensePath.
import fs from 'node:fs/promises'
import path from 'node:path'
export async function activate(product, manifest) {
// A licensing SDK set up for the product you just installed
const licensing = new MoonbaseLicensing({ productId: product.id, endpoint: ENDPOINT, publicKey: PUBLIC_KEY })
const deviceToken = (await licensing.generateDeviceToken()).toString()
const activation = await moonbase('/api/customer/inventory/activate?method=Online', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(deviceToken),
})
if (!activation.ok)
throw new Error(`Activation failed: ${activation.status}`)
const licenseFile = await moonbase(activation.headers.get('location'))
await fs.mkdir(path.dirname(manifest.licensePath), { recursive: true })
await fs.writeFile(manifest.licensePath, await licenseFile.text())
}using System.Net.Http.Json;
using System.Text;
async Task Activate(string productId, string licensePath)
{
// A licensing SDK set up for the product you just installed
var product = LicensingFactory.CreateInstance(opts =>
{
opts.Endpoint = new Uri(Endpoint);
opts.ProductId = productId;
opts.AccountId = "acme";
opts.PublicKey = PublicKey;
});
var deviceToken = Encoding.UTF8.GetString(await product.GenerateDeviceToken());
using var activation = await moonbase.PostAsJsonAsync("/api/customer/inventory/activate?method=Online", deviceToken);
activation.EnsureSuccessStatusCode();
var licenseFile = await moonbase.GetStringAsync(activation.Headers.Location);
Directory.CreateDirectory(Path.GetDirectoryName(licensePath)!);
await File.WriteAllTextAsync(licensePath, licenseFile);
}Your product reads the file at licensePath when it starts, with your SDK's readRawLicense (ReadRawLicense in .NET), the same call that reads an offline activation's license file. From then on it validates the license like any other.
Use a current Moonbase licensing SDK in both. The current SDKs all work out the same device ID for a computer, so a license your install manager activates is valid in your product, whichever language each is written in. A product on Moonbase's original JUCE licensing module works out a different one, so let it activate itself.
Activating again is safe. A computer that's already activated gets its existing activation back, without using another seat, so you can activate whenever the license file is missing.
Leave
licensePathempty if your product activates itself (the customer signs in or enters a key in your product). Your install manager then only installs and updates it.
More you can add
Show what they don't own yet.
GET /api/customer/storefrontlists every product for sale, each withowned, and its price indefaultVariation. A Buy button can openhttps://<account-id>.moonbase.sh/buy/<product-id>in the browser.Update the install manager itself. It's a product, so publish its releases in Moonbase like any other product's. Compare your app's version with the
currentVersionfromGET /api/customer/inventory/products/<its-product-id>, and install its download the same way.Uninstall. Remove the files the manifest lists, and the license file. Customers can free the computer's activation in their account.
Good to know
Treat the license token like a password. It opens the customer's purchases and licenses. Keep it where your app keeps its license, don't log it, and only send it to your Moonbase address.
Signing out frees the computer. Revoke the install manager's license with your SDK (
revokeLicense, orRevokeLicensein .NET), then delete the stored license.Your own API can accept the same token. See "Calling your API from your app" in Authenticate storefront customers on your own API.
Test it as a customer. Install, update and activate each product from a customer account that owns it, on a Mac and a PC.
For your SDK's activation, validation and offline activation calls, see the licensing SDK documentation.
Troubleshooting
Every call answers 401
Send the token with the LicenseToken scheme, not Bearer. It has to be your install manager's license token, from the same Moonbase account you're calling.
Activation fails with "No license with available activations found"
The customer doesn't own the product, or every activation on their license is in use. They can free one in their account.
Your product doesn't accept the license file
Check that your product reads it from the same licensePath, and that both apps use a current licensing SDK (see step 5).
A product never shows as installed
Its manifest lists a file the installer doesn't place on that platform, or the product has no manifest for that platform.
An update never goes away
The version in your binaries doesn't match the release's. Give them the same version number.
Was this helpful?
More in Guides
Selling add-ons with sub-productsImport newsletter subscribersStill need help? Ask the team