Skip to content

Branded desktop product

A branded product is your extension shipped as its own desktop application: its own name in the dock, its own icon, its own window with no WAMP chrome, its own data directory and its own update feed. One declarative file describes the whole brand, and one script runs and builds it.

products/<brand>/product.json. Its schema is closed — an unknown key is a build error, not an ignored line — and it is validated at build time, so a malformed brand fails the build rather than the installed app. The validated product is baked into the main bundle; there is no runtime brand switching, and only the display fields ever reach the renderer.

Every field, and what it controls:

Field Controls
id the stable machine slug (for example, wamp). Used on any wire that needs one, including catalog scoping. Never changes for branding reasons
name the application name — dock, menu bar, and the userData directory. Also the packaged artifact name
appId the bundle identifier
executableName the packaged binary’s name
icons a repository-relative directory holding icon.png, icon.ico, icon.icns
iconVariants optional 1–12 { id, label, file } entries with unique ids; PNG basenames under icons/app-icon-variants/. The first matches the packaged default
wordmark logo text; null renders the mark alone
copyright the copyright string in the binary’s metadata
rootPageId the switch. null is the full WAMP shell. Set, and the app boots straight into that page’s window
bundledExtensions extension ids staged into the package and seeded on first run
settingsTabs an allowlist of settings tab ids; null shows every tab
serverUrl the default API base. The WAMP_API_URL environment variable still overrides it at runtime
cspConnectHosts extra connect-src hosts for the packaged renderer’s content security policy
homeDirSegment the user-home store segment — .wamp becomes ~/.wamp. A dot-prefixed segment with no path separators. This is never the workspace-local .wamp/ directory, which is a data format inside a user’s project
localCorePort the loopback port the engine daemon listens on. Explicit and unique per product
update.url / update.channel the auto-update feed and channel

rootPageId is the field that makes it a product

Section titled “rootPageId is the field that makes it a product”

With rootPageId: null you get WAMP with your branding on it: the main window, the sidebar, the chat. Set it to one of your extension’s page ids and the app boots with no main window at all — the root page’s window is the application. Secondary windows your app opened last session are restored around it.

The page has to come from somewhere, which means the extension that contributes it must be in bundledExtensions. The seed of those extensions is ordered to land before the extension loader’s first scan, so on a first run the root page exists without any network round-trip. Page ids and window behavior are in Pages and windows.

Terminal window
npm run new:product -- atlas

That writes products/atlas/product.json with the default product as its template: the same serverUrl and cspConnectHosts, homeDirSegment set to .atlas, update.url pointed at <serverUrl>/releases/atlas, and localCorePort set to one above the highest port any existing product uses — so two brands can never collide on the loopback port. It also copies placeholder icons into products/atlas/icons/, so the product builds on the day it is created, and prints the two commands to run next.

A new product starts as the full shell with no bundles. Then you edit it: the name, the wordmark, bundledExtensions, and rootPageId.

To create a product around an existing app extension in one step:

Terminal window
node scripts/product.mjs create atlas --name Atlas --extension atlas-app
node scripts/product.mjs status atlas

create selects the extension’s first declared page, bundles it, and sets rootPageId; order the pages accordingly. For an extension outside the checkout, pass --extension-dir /absolute/path/to/atlas-app. The source must contribute a page. External sources are staged immediately; build a sealed extension before staging it, because create does not run its custom build.

bundledExtensions is a list of extension ids. Each one is resolved at package time from one of two places:

  • extensions/<id> — an in-repo source. It is built and staged into the package automatically by the packaging hook.
  • products/<brand>/extensions/<id> — a tree pre-staged by that extension’s own script, for an app that lives outside the repository.

A declared bundle that is in neither place fails the build with a message naming both paths. The staged form is the same published form the marketplace serves, so an extension does not need a second build to be bundled.

Pre-staged bundles are a snapshot, which used to be the one silent failure in the pipeline — edit the app, build, and ship the old code. The build now refuses instead: if the source it was staged from is newer than the staged copy, the build stops and prints the re-stage command.

On first run each bundle is copied into the user’s extension directory before the loader scans, and on later runs it is replaced when the bundled copy has a newer version. Extension storage lives outside the extension directory and is never touched by the seed, so an update does not cost a user their data.

Terminal window
npm run start:product -- atlas

That is the exact equivalent of npm start, with your product instead of WAMP.

Terminal window
npm run build:product -- atlas
npm run build:product -- atlas --platform mac --arch arm64
npm run build:product -- atlas --platform win --arch x64
npm run build:product -- atlas --platform linux --arch x64

--platform accepts mac, win or linux and defaults to the machine you are on. --arch accepts arm64 or x64; it defaults to the host architecture except on Windows, which is always x64. Anything else is rejected with the valid values. macOS produces a DMG and a ZIP, Windows an NSIS installer, Linux an AppImage — the one Linux format that auto-updates.

Two directories come out, both derived from the product file rather than hardcoded:

  • out/<name>-<platform>-<arch> — the packaged application tree.
  • dist/<brand> — the distributables. The default product keeps a plain dist/.

Before it builds anything the script checks the things that otherwise fail late and unhelpfully: a missing icons directory, a bundle it cannot resolve, a stale staged copy. To see what a build would do without running it:

Terminal window
node scripts/product.mjs plan atlas --platform mac --arch arm64

That prints the derived paths, the resolved bundle sources, and the exact argument arrays as JSON, and touches nothing.

The app finds its feed through a generated app-update.yml inside the package, written from update.url and update.channel. The updater then polls <update.url>/latest*.yml every four hours. Nothing in the application code sets a feed URL — the product file is the only place it is decided.

Terminal window
bash scripts/upload-release.sh atlas

The script uploads dist/atlas’s current latest*.yml manifests, the artifacts they reference, and their .blockmap siblings, over ssh to WAMP’s release host — so this exact command is for whoever operates that host. If you serve your own feed, point update.url at it and copy the same files: artifacts first, then the manifests, so a client polling mid-upload never reads a manifest pointing at a file that is not there yet.

Two guards run before anything is copied, and both have caught real mistakes: a latest*.yml whose version does not match the repository’s current version is skipped loudly rather than uploaded, because a stale manifest repoints an entire platform’s update channel at an old release; and every artifact name must start with the product’s name, which is what stops one product’s build from landing in another’s channel.

Local packaging without APPLE_IDENTITY uses an ad-hoc signature. With APPLE_IDENTITY="Developer ID Application: Name (TEAMID)", packaging signs with that identity and enables the hardened runtime. That local command does not by itself notarize the output.

The Desktop release workflow signs with Developer ID, notarizes and staples the application before making distributables, then notarizes and staples the DMGs. It verifies signatures and architecture before upload. Its signing steps are part of the release path, not evidence that an arbitrary local build is signed. A branded release must supply its own signing credentials and run the equivalent checks for its own artifacts.

Automatic updates are enabled for packaged release builds marked at build time with WAMP_RELEASE_BUILD=1. Development builds, local packages, and unsigned CI smoke artifacts use updater stubs. This gate applies across operating systems; macOS is no longer categorically excluded. Only use the release flag for artifacts whose signing and publishing path you actually control.

The macOS updater treats a URL containing arm64 as arm64 and other URLs as x64. Keep the architecture explicit in published artifacts:

Target arm64 x64
DMG <Name>-<version>-arm64.dmg <Name>-<version>-x64.dmg
ZIP <Name>-<version>-arm64-mac.zip <Name>-<version>-x64-mac.zip

A generic DMG alias is for manual download and must not substitute for an architecture-specific updater entry. build:product derives the packaged tree and distributable architecture from the same --arch value; keep using that entry point instead of pairing them manually.

Both architecture builds write latest-mac.yml, so merge the manifests after building both. Publish one manifest containing both ZIPs, and verify each ZIP’s binary architecture against its filename. The default product’s release workflow performs this sequence; a branded release must use its own product paths and feed.

The NSIS installer is named <Name> Setup <version>.exe. Packaging signs the application executables when WINDOWS_CERTIFICATE_FILE and WINDOWS_CERTIFICATE_PASSWORD are supplied; WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD configure signing of the NSIS installer and uninstaller. The release workflow verifies both the packaged executables and installer with Authenticode. An unsigned local package is not a signed release.