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.
What a product file is
Section titled “What a product file is”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.
Scaffold a brand
Section titled “Scaffold a brand”npm run new:product -- atlasThat 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:
node scripts/product.mjs create atlas --name Atlas --extension atlas-appnode scripts/product.mjs status atlascreate 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.
Bundle your app into it
Section titled “Bundle your app into it”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.
Run it
Section titled “Run it”npm run start:product -- atlasThat is the exact equivalent of npm start, with your product instead of WAMP.
Build the binary
Section titled “Build the binary”npm run build:product -- atlasnpm run build:product -- atlas --platform mac --arch arm64npm run build:product -- atlas --platform win --arch x64npm 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 plaindist/.
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:
node scripts/product.mjs plan atlas --platform mac --arch arm64That prints the derived paths, the resolved bundle sources, and the exact argument arrays as JSON, and touches nothing.
Publish to an update feed
Section titled “Publish to an update feed”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.
bash scripts/upload-release.sh atlasThe 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.
macOS signing and updates
Section titled “macOS signing and updates”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.
Architecture and filenames
Section titled “Architecture and filenames”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.
Shipping both macOS architectures
Section titled “Shipping both macOS architectures”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.
Windows
Section titled “Windows”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.