Design: Architecture
This page describes Cloudpack's technical architecture for contributors and advanced users who want to understand how the system works internally.
Overview
Cloudpack uses a multi-server architecture to separate concerns and enable features like cross-repo development. The key components are:
┌───────────────────────────────────────┐
│ Browser │
│ ┌─────────────────────────────────┐ │
│ │ App code + Import map + Overlay │ │
│ └────────┬──────────────┬─────────┘ │
└───────────┼──────────────┼────────────┘
│ │
Requests: │ │ Requests:
HTML/Routes ────────┘ └───────── Bundles
│ │
┌───────────▼──────────────▼────────────┐
│ Development Servers │
│ ┌────────────┐ ┌───────────────┐ │
│ │ App Server │ │ Bundle Server │ │
│ │ :5000 │ │ :5500 │ │
│ └─────┬──────┘ └───────┬───────┘ │
│ │ │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ API Server │ │
│ │ (orchestration) │ │
│ └────────┬────────┘ │
└──────────────────┼────────────────────┘
│
┌──────────────────▼────────────────────┐
│ Context (Session, Cache) │
└───────────────────────────────────────┘Multi-server architecture
When you run cloudpack start, up to three Express servers are started to handle different responsibilities.
App server
The app server hosts your application's routes and serves HTML pages.
Responsibilities:
- Serve HTML pages with embedded import maps
- Handle application routes (including custom rendering if configured)
- Inject the Cloudpack overlay script for status and error display
- Optionally serve bundles (if
useSingleWebServerfeature is enabled)
Default ports: 5000-5003 (configurable via server.port in Cloudpack config or --app-server-port)
Key files: startServers and createRoutes
Bundle server
The bundle server handles requests for package bundles. It's the workhorse of Cloudpack's library mode. (If the useSingleWebServer feature is enabled, the bundle requests are handled by the app server.)
Responsibilities:
- Parse bundle request URLs (format:
/{packageName}@{version}/h-{hash}/v{refreshVersion}/{bundled|unbundled}/{file}) - Call
ensurePackageBundledto either bundle the package or get the cached version - Serve bundles with appropriate caching headers
- Handle linked package redirects
- Handle worker requests
Default ports: 5500-5503 (configurable via --bundle-server-port)
Key files: startBundleServer and handleBundleRequest
API server
The API server provides internal orchestration and real-time communication with the browser overlay.
Responsibilities:
- Provide a tRPC server which exposes a WebSocket with API endpoints for clients, such as the browser overlay UI or
link/unlinkCLI - Provide a data bus for internal communication
- Publish bundling status updates, errors, and notifications to the WebSocket via the data bus
Default ports: 9890-9893 (configurable via --api-server-port)
Key files: createCloudpackServer to create the server, createCloudpackClient to connect to the server, and list of APIs
Other contents of the api-server package
The @ms-cloudpack/api-server package also contains a lot of core logic for bundling, as well as Context creation. However, bundling is not currently orchestrated with API server requests.
The main entry point for bundling is ensurePackageBundled, and requests are managed and de-duplicated by the TaskRunner.
Single web server mode
For environments where cross-origin requests cause issues, enabling the useSingleWebServer feature routes bundle requests through the app server instead of a separate bundle server. (There will still be a separate API server.)
Request flow
Startup
When cloudpack start runs (source):
- Load configuration - Read
cloudpack.config.jsonand merge with defaults - Create context - Initialize context and session state: package definitions cache, task runner, data bus, cache helpers, etc
- Resolve dependencies - During context initialization, build the app's resolve map (dependency graph) from
node_modules - Start servers - Launch API server, app server, bundle server
- Pre-bundle the app package - Call
ensurePackageBundledfor the app package - Open browser - Navigate to the app URL
Page load
When the browser loads a page:
- Browser requests HTML from app server
- App server returns HTML page with:
- Content as specified in the
routesconfig - Import map (
<script type="importmap">) containing package to URL mappings generated bycreateImportMap - Bootstrap script that creates a Cloudpack client connected to the API server WebSocket
- Overlay component for status display
- Content as specified in the
- Browser executes app code and encounters imports
- Import map resolves bare specifiers (e.g.
react) to bundle URLs - Browser requests bundles from bundle server
Bundle request handling
When the bundle server receives a request:
GET /react@18.0.0/h-abc123/v1/bundled/index.js- Parse URL - Extract package name, version, hash, and file path
- Validate - Check if package exists in resolve map
- Check cache - Look for existing bundle matching the hash
- Bundle if needed - If no valid cache, run the bundler
- Cache result - Save bundle to disk cache
- Serve response - Return bundle with caching headers
File change (watch mode)
When you edit a file:
- Watcher detects change - File watcher notifies the system
- Invalidate cache - Mark affected package bundles as stale
- Re-bundle - TaskRunner queues a new bundle task
- Notify browser - Publish update via data bus
- Update page - Overlay receives notification via
reloadCountSourceand either:- Applies Hot Module Replacement (if enabled and supported)
- Triggers a full page reload
If the Cloudpack config is edited, the entire start command will restart.
Context object
The Context object is the central state container passed to all server operations. It's created by createApiContext and contains:
| Property | Description |
|---|---|
session | Current session state (config, resolve map, import map, URLs) |
reporter | Console output and task logging |
packages | Package definitions (package.json) cache |
taskRunner | Manages bundling and other async tasks |
packageHashes | Cached package hash calculations |
bus | Data bus for pub/sub messaging with client |
watcher | File system watcher |
telemetryClient | Manages telemetry tracking if enabled |
remoteCacheClient | Manages the remote cache connection and auth if enabled |
Session state
The Session object holds runtime state that can change during a dev session. Partial contents:
| Property | Description |
|---|---|
config | Merged configuration from files and defaults |
resolveMap | Dependency graph with resolved package locations and versions |
importMap | Import map for the browser |
urls | Server URLs (app, bundle, API) |
linkedPaths | Externally linked package paths (from cloudpack link) |
cachePath | Location of the local bundle cache |
sessionVersion | Used to force refreshing all bundles on the client |
targetVersions | Used to force refreshing one package (target) on the client |
sequence | Used to force a refresh on the client via reloadCountSource |
Key file: createSession
Resolve map
The ResolveMap is the source of truth for the dependency graph. It maps package specifiers to their resolved locations and metadata, using exports maps or main/module settings if available, or various heuristics if not.
The import map is generated from the resolve map, converting package paths to bundle server URLs.
Expand for examples
Partial resolve map:
{
"my-app": {
"version": "1.0.0",
"path": "/repo/packages/my-app",
"dependencies": {
"react": "17.0.0",
"library-a": "1.0.0"
}
},
"library-a": {
"version": "1.0.0",
"path": "/repo/packages/library-a",
"dependencies": {
"react": "18.0.0"
},
"requiredBy": {
"my-app@1.0.0": "^1.0.0"
}
},
"react": {
"version": "17.0.0",
"path": "/repo/node_modules/react",
"isExternal": true,
"dependencies": {
"loose-envify": "1.4.0"
},
"requiredBy": {
"test-app@1.0.0": "^17.0.0"
},
"scopedVersions": {
"18.0.0": {
"version": "18.0.0",
"path": "/repo/node_modules/library-a/node_modules/react",
"isExternal": true,
"dependencies": {
"loose-envify": "1.4.0"
},
"requiredBy": {
"library-a@1.0.0": "^18.0.0"
}
}
}
}
}Import map
An import map is a modern browser feature that tells the browser how to resolve module specifiers in ESM import statements. It goes inside a <script type="importmap"> tag and looks like this (from the resolve map example above):
{
"imports": {
"my-app/lib/index.js": "/my-app@1.0.0/h-pending/v0.15/bundled/lib/index.js",
"library-a": "/library-a@1.0.0/h-pending/v0.15/bundled/lib/index.js",
"loose-envify": "/loose-envify@1.4.0/h-v12-abc123/v0/bundled/index.js",
"react": "/react@17.0.0/h-v12-def123/v0/bundled/index.js",
"react/jsx-runtime": "/react@17.0.0/h-v12-def123/v0/bundled/jsx-runtime.js"
},
"scopes": {
"/library-a@1.0.0": {
"react": "/react@18.0.0/h-v12-abc456/v0/bundled/index.js",
"react/jsx-runtime": "/react@18.0.0/h-v12-abc456/v0/bundled/jsx-runtime.js"
}
}
}How import maps work at runtime
See the bundling page for how the import map is created by Cloudpack and used by the browser.
URL format
Bundle URLs generated by createImportMap follow this pattern:
/{packageName}@{version}/h-{hash}/v{sessionVersion}{.targetVersion}/{bundleType}/{entryPath}hash- Ensures browser fetches new bundles when package changes- For internal packages it will start as
"pending". After each re-bundle (such as on watched file change),addImportMapHashis called to update the hash in the server's copy of the import map, which is sent to the browser on page reload.
- For internal packages it will start as
sessionVersion- Forces refresh when session state changes (e.g., after linking or restarting all tasks).targetVersion- Optional segment specific to the package (target), based on number of times it's been forced to re-bundle for reasons other than content changesbundleType-"bundled"or"unbundled"depending on original module typeentryPath- Path to the package entry file used at runtime, as detected bygetRuntimeEntryPaths
Variants:
- If
features.useSingleWebServeris enabled, all URLs are prefixed with/__bundled__/ - If
features.relativeImportMapPathsis disabled, the paths are full URLs
Caching strategy
Cloudpack's speed comes from aggressive caching at multiple levels.
Package hashing
Each package is hashed before bundling. This ensures the cached bundle output is invalidated when anything that will affect it changes.
The hash includes the following (see hashPackage and getPackageHashEntries for full list and implementation):
- Metadata: name, version, exports map, and dependencies
- Source file contents for internal/linked packages only (for external packages, the contents would only change when the version changes)
- Patch file contents for external packages with patches
- Package settings and bundler capabilities
- Target environment and HMR settings
Cache levels
- Browser cache - Bundles served with
max-age=31536000(1 year) when the URL includes a hash. Changed packages get new hashes, so old cached versions don't conflict. - Local disk cache - Bundled output stored in
~/.cloudpack/{name}-${version}-{hash}/. Before bundling, Cloudpack checks if a valid cached bundle exists. - Remote cache - Azure Blob Storage can store pre-built bundles. Teams can share caches via
cloudpack sync:- Upload during CI:
cloudpack sync --upload - Download on dev machines:
cloudpack sync(often inpostinstall)
- Upload during CI:
Cache invalidation
- On package change: When a package's hash changes, its bundle output is invalidated. During
start, the file watcher will notify about these changes to internal packages, invalidate the cache, and notify the API server to refresh the browser. - On browser interaction: Certain user interactions with the browser or overlay UI during
startcan force cache invalidation using theSessionpropertiessessionVersion,targetVersions, andsequence. - On
cloudpack linkchanges: Uses the same mechanism as above - On internal changes to Cloudpack: There's a hardcoded global
hashVersionin the Cloudpack source which can be used to invalidate all cache output in case of breaking internal changes within Cloudpack, such as default settings passed through to the underlying bundlers.
Data bus
The data bus stores data in a tree of named paths and provides pub/sub over that tree: publishers set the value at a path, and subscribers to any branch get notified when data underneath it changes. (It can also use providers to fetch data on demand, though Cloudpack only uses direct publishing.)
Publishers emit events to named sources. In Cloudpack, these are defined in api-server busSources.ts.
const reloadCountSource = createDataPath({
path: ['session', 'version'] as const,
type: zod.number(),
})
bus.publish(reloadCountSource, 2)Subscribers receive updates:
bus.subscribe(reloadCountSource, (count) => {
context.reporter.reset()
})The API server allows the client to subscribe to data bus updates via CloudpackClient onDataChanged:
client.onDataChanged.subscribe(reloadCountSource, {
onData: (data) => {
if (Number(data) > currentSequence) {
window.location.reload()
}
},
})For more details, see the data-bus readme
Hot module replacement
When HMR is enabled (features.hmr: true), Cloudpack can update code in the browser without a full page reload. This feature is still experimental and currently has some significant limitations; see the HMR GitHub issue for details.
Flow
- File change detected - Watcher notifies the system
- Re-bundle package - Only the changed package is rebuilt
- Publish update -
notifyHostemits affected entry points to the data buspackageUpdateSource - Browser receives - Overlay client gets the update via WebSocket
- Apply update -
handlePackageUpdateusesreact-refreshto apply changes - Fallback - If HMR fails, falls back to full page reload
Notes and limitations
- Only supported with Rspack (will be selected automatically for internal packages when HMR is enabled)
- Changes to eagerly-loaded React components work best; non-component changes or changes to lazy-loaded components typically require full reload
- Component state is preserved across updates
- Automatic fallback to full page reload on errors