Skip to main content

Build targets & platforms

One of postman-app's most interesting engineering challenges is this: the same codebase needs to power multiple, very different products. A desktop app. A browser app. An enterprise self-hosted version. A VS Code extension. Understanding how this works explains many architectural decisions that might otherwise seem arbitrary.


The products built from postman-app

Postman (Orange) — The main desktop app. What most users download from postman.com. Runs on macOS, Windows, and Linux via Electron. Full feature set including cloud sync, team collaboration, and the request engine.

Postman (Browser/Web) — The same app accessible at app.postman.com. No Electron. No local filesystem. No native menus. Same React code, served from a CDN.

Postman Black — An enterprise version for security-conscious customers who cannot use Postman's cloud infrastructure. Self-hosted on the customer's own servers. A trimmed version of the app that replaces Postman's backend URLs with the customer's own services. Excludes cloud-only features.

VS Code Extension — Parts of Postman running inside VS Code. Uses some of the same libs/ and data/ packages, but in a completely different host environment.


How one codebase becomes four products

The key is build-time configuration. When the build runs, it compiles constants into the JavaScript bundle:

TARGET_PLATFORM // 'desktop' | 'browser'
__WP_ELECTRON__ // true | false
__WP_ENV__ // 'staging' | 'production'

Feature code reads these constants to decide what to render:

import { SyncFeature } from '@postman-app/collaboration';

// This component doesn't render at all in the browser build
// because __WP_ELECTRON__ is false there
if (__WP_ELECTRON__) {
return <LocalFileAccessFeature />;
}

// This always renders
return <CloudSyncFeature />;

Dead code elimination at build time means the browser bundle doesn't contain any Electron-specific code, and vice versa. The bundles are genuinely different, not just conditionally hidden.


The desktop vs browser difference

These are the meaningful differences between the two builds:

CapabilityDesktopBrowser
Local filesystem access✅ Via main process IPC
Local workspace (files on disk)
Postman Agent (local request runner)❌ (uses cloud agent)
Native file dialogs
Auto-updater✅ Electron auto-update❌ (CDN deploy)
Native desktop notifications⚠️ Browser notifications
Custom URL protocol (postman://)
Offline mode✅ (partial)
Cloud features (sync, collaboration)

Why this matters for your code

When you write code in a ui-feature or data/ package, it needs to work in all targets where that package is included. This means:

Never import Node.js APIs directly in renderer code. require('fs'), require('path'), require('child_process') — none of these are available in the browser build. If you need filesystem access, go through IPC (the platform packages handle this).

Check __WP_ELECTRON__ before using desktop-only features. If your feature only makes sense on desktop, guard it:

if (!__WP_ELECTRON__) {
return <BrowserFallbackUI />;
}
return <DesktopOnlyFeature />;

Don't assume the local filesystem exists. Some features support "local mode" (editing files on disk) and "cloud mode" (data synced to Postman's servers). Data packages like data/collection-data have separate service implementations for each mode.


Postman Black: the enterprise variant

Postman Black deserves special attention because it's the reason the decoupling migration matters so much.

Postman Black is a completely self-hosted version of Postman for enterprises. The customer runs Postman's backend on their own infrastructure. The app talks to the customer's servers, not Postman's.

From a build perspective, Postman Black uses:

  • config/environments/enterprise/ — different backend URLs
  • Different feature flags — cloud-only features are disabled
  • A different app identity (different icon, name, protocol)

The critical constraint: Postman Black does not have src/renderer/. The monolith is not included in Postman Black builds.

This is why "decoupling" is not just architectural elegance — it's a business requirement. Any code that imports from @postman-app-monolith/* or @@renderer/* cannot run in Postman Black. Every time the team decouples a package from the monolith, that package becomes available for Postman Black too.

This is also why the VS Code extension exists as a separate concern — it can only use packages from libs/ and data/ that have zero dependencies on Electron or the monolith.


The packages/ directory: squad organization

You will notice that packages/ is organized by squad rather than by module type:

packages/
├── api-client/ ← API Client team's packages
├── collaboration/ ← Collaboration team's packages
├── runtime/ ← Runtime team's packages
└── ...

This is the older organization pattern from before the Nx layer system was established. Code here is being gradually moved into the appropriate Nx layer (views/, ui-features/, data/, etc.) as it gets touched. The packages/ directory is not the preferred home for new code.


Summary: one codebase, multiple products

postman-app source code

├── yarn start (desktop)
│ ↓
│ TARGET_PLATFORM=desktop + __WP_ELECTRON__=true
│ ↓
│ Rspack builds → Electron wraps → PostmanDev.app

├── yarn start --target browser
│ ↓
│ TARGET_PLATFORM=browser + __WP_ELECTRON__=false
│ ↓
│ Rspack builds → CDN deploys → app.postman.com

└── Enterprise build

config/environments/enterprise/ + no src/renderer/

Rspack builds → Electron wraps → Postman Black

The same React components. The same Nx packages. Different configuration, different target, different product.