# Branded desktop product Configure and build a branded Desktop product, then prepare signed artifacts and a compatible update feed. Source: https://docs.vampikez.fun/ship/branded-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. :::note The product file, the build pipeline, and the release upload script live inside the WAMP monorepo. Producing a branded binary today requires access to that repository — there is no published CLI that packages a product from outside it. The commands on this page are the ones that repository accepts, so they are accurate rather than illustrative, and the fields are worth reading either way: they are the whole configuration surface of a branded build. ::: ## What a product file is `products//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 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](/build/pages-and-windows/). ## Scaffold a brand ```bash 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 `/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: ```bash 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. ## 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/` — an in-repo source. It is built and staged into the package automatically by the packaging hook. - `products//extensions/` — 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 ```bash npm run start:product -- atlas ``` That is the exact equivalent of `npm start`, with your product instead of WAMP. ## Build the binary ```bash 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/--` — the packaged application tree. - `dist/` — 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: ```bash 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. ## 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 `/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 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. ## 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 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 | `--arm64.dmg` | `--x64.dmg` | | ZIP | `--arm64-mac.zip` | `--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 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 The NSIS installer is named ` Setup .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.