How the app is built
When you run yarn start and the Postman app opens on your screen, a lot happened between those two events. This page tells that story — from TypeScript files to a running desktop application.
The two apps in one
postman-app is not one application. It is two, built from the same codebase, for different purposes:
The desktop app (Postman Orange) — The Electron application that installs on macOS, Windows, and Linux. This is what most people mean when they say "Postman." It has access to the filesystem, can open native windows, runs the request engine locally, and auto-updates in the background.
The web app — The browser version of Postman, accessible at app.postman.com. No Electron, no filesystem access, no native menus. The same React code, deployed to a CDN, running in Chrome or Firefox.
The same source code (the src/renderer/ monolith and all the Nx packages) builds both. A build flag called TARGET_PLATFORM tells the bundler which version you're building:
yarn start # TARGET_PLATFORM=desktop (default)
yarn start --target browser # TARGET_PLATFORM=browser
Code that checks this flag:
if (__WP_ELECTRON__) {
// Only runs on desktop — native Electron APIs available
}
The build system: Rspack
The bundler is Rspack — a Rust-based replacement for Webpack that is configuration-compatible (same syntax) but dramatically faster.
2022: Webpack 4 → P95 build time: 29 seconds
2023: Webpack 5 → P95 build time: 16 seconds
2024: Rspack → P95 build time: 5 seconds
The switch to Rspack was not a big-bang rewrite. Because Rspack accepts the same configuration format as Webpack, the migration happened incrementally. The config lives in config/rspack/.
Rspack uses SWC as the TypeScript/JavaScript transpiler (instead of Babel). SWC is written in Rust and is 20-70x faster than Babel. The combination of Rspack + SWC is what makes a 5-second full rebuild possible on a large codebase.
What gets bundled
The build produces multiple bundles — not one giant file, but separate chunks for different parts of the app:
Critical path bundle — Loads on startup. Contains the minimum needed to show the app. Bundle size is monitored by Statoscope on every PR — if the critical path bundle grows by more than 5kB, the PR fails. This is intentional: startup time is critical to user experience.
Feature chunks — Each ui-feature lazy-loads its own chunk. When you click on a feature for the first time, the browser downloads that chunk. This is why ui-features/ are loaded asynchronously (import('...')).
Other chunks — The runner, console, and other subsystems have their own bundles. You can skip bundling them during development to speed up builds:
yarn start -- --skip-bundle="console,runner"
How the desktop app is packaged
Bundling produces JavaScript + HTML + CSS. That's the web app. To turn it into a desktop app, we use Electron and Starship.
Starship is what Postman calls the packaging system. The configs in starship/ tell Electron how to wrap the web bundle:
// starship/dev.json
{
"name": "PostmanDev",
"version": "2.3.0",
"windows.iconset": "../resources/dev_windows.iconset",
"macos.iconset": "../resources/dev_macos.iconset",
"linux.iconset": "../resources/dev_linux.iconset",
"protocol": "postman-dev",
"directory": "../build/platform"
}
Each channel (dev, beta, canary, stage, prod) has its own config. This is why PostmanDev, PostmanBeta, and Postman can all be installed side by side — they have different identifiers, different icons, different URL protocols.
To package the app:
yarn run starship-package # Package for current channel (dev)
yarn run starship-package -c beta # Package PostmanBeta
yarn run starship-package -c prod # Package production Postman
The environment config system
The built app needs to know which backend URLs to talk to. This is handled by the environment config system in config/environments/.
config/environments/
├── default/
│ ├── desktop/
│ │ ├── base.json ← defaults for all desktop channels
│ │ ├── dev.json ← dev overrides
│ │ ├── beta.json
│ │ ├── prod.json
│ │ └── ...
│ └── browser/
│ ├── base.json
│ └── ...
├── enterprise/ ← Postman Black enterprise builds
└── gw/ ← Gateway variant
At build time, the configs are merged and compiled into the bundle as global constants:
// You can use these anywhere in the app code:
__WP_ENV__ // 'staging' | 'production'
__WP_ELECTRON__ // true | false
__WP_RELEASE_CHANNEL__ // 'dev' | 'beta' | 'prod' | ...
__WP_HTTP_GATEWAY_URL__ // 'https://bifrost-https-v10.gw.postman.com'
__WP_SYNCSERVER_URL__ // 'https://bifrost-v10.getpostman.com'
TARGET_PLATFORM // 'desktop' | 'browser'
For local development, create config/environments/local.json to override any value:
{
"__WP_HTTP_GATEWAY_URL__": "'https://localhost:3000'"
}
This file is gitignored — your overrides stay local.
What Postman Black is
Postman Black is a second version of Postman for enterprise customers who need to self-host — typically large companies with strict security requirements that cannot send API requests through Postman's cloud servers.
From a code perspective, Postman Black is a different build of the same postman-app repository. It uses a different config (config/environments/enterprise/) with different backend URLs (pointing to the customer's own infrastructure) and excludes features that require Postman's cloud (like sync, comments, and team workspaces that go through Postman's servers).
This is one of the major reasons the decoupling migration matters: any code in src/renderer/ cannot be used in Postman Black, because it imports the full monolith. Decoupled Nx packages can be shared between Postman Orange and Postman Black.
Summary: from yarn start to running app
yarn start→ Rspack reads the entry point- Rspack bundles TypeScript + JSX → JavaScript chunks
- Config values (
__WP_ENV__,__WP_ELECTRON__, etc.) are compiled in - Bundle is served on a local dev server (
https://matrix.postman-beta.co:8777) yarn open→ Electron opens a window pointing at the dev server URL- The main process (
src/main/) starts, opens the window, sets up IPC - The renderer loads → React bootstraps → the app is ready
For production packaging, step 4 is replaced by Starship packaging into a native binary (.app, .exe, .deb), which is then signed, notarized, and uploaded to the update server.