Develop web applications with seamless Salesforce integration
The sf ui-bundle dev command enables local development of modern web applications (React, Vue, Angular, etc.) with automatic Salesforce authentication. It intelligently discovers your webapp configuration, handles proxy routing, injects authentication headers, and supports hot reload - so you can focus on building your app.
- Auto-Discovery: Automatically finds webapps in
webui/folder - Optional Manifest:
webapplication.jsonis optional - uses sensible defaults - Auto-Selection: Automatically selects webapp when running from inside its folder
- Interactive Selection: Prompts with arrow-key navigation when multiple webapps exist
- Authentication Injection: Automatically adds Salesforce auth headers to API calls
- Intelligent Routing: Routes requests to dev server or Salesforce based on URL patterns
- Hot Module Replacement: Full HMR support for Vite, Webpack, and other bundlers
- Error Detection: Displays helpful error pages with fix suggestions
- Framework Agnostic: Works with any web framework
my-sfdx-project/
├── sfdx-project.json
└── force-app/main/default/webui/
└── my-app/
├── my-app.webapplication-meta.xml
├── package.json
├── src/
└── webapplication.json
sf ui-bundle dev --target-org myOrg --openBrowser opens to http://localhost:4545 with your app running and Salesforce authentication ready.
Note:
{name}.webapplication-meta.xmlis required to identify a valid webappwebapplication.jsonis optional for dev configuration. If not present, defaults to:
- Name: From meta.xml filename or folder name
- Dev command:
npm run dev- Manifest watching: Disabled
sf ui-bundle dev [OPTIONS]| Option | Short | Description | Default |
|---|---|---|---|
--target-org |
-o |
Salesforce org alias or username | Required |
--name |
-n |
Web application name (folder name) | Auto-discover |
--url |
-u |
Explicit dev server URL | Auto-detect |
--port |
-p |
Proxy server port | 4545 |
--open |
-b |
Open browser automatically | false |
# Simplest - auto-discovers webapplication.json
sf ui-bundle dev --target-org myOrg
# With browser auto-open
sf ui-bundle dev --target-org myOrg --open
# Specify webapp by name (when multiple exist)
sf ui-bundle dev --name myApp --target-org myOrg
# Custom port
sf ui-bundle dev --target-org myOrg --port 8080
# Explicit dev server URL (skip auto-detection)
sf ui-bundle dev --target-org myOrg --url http://localhost:5173
# Debug mode
SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrgThe command discovers webapps using a simplified, deterministic algorithm. Webapps are identified by the presence of a {name}.webapplication-meta.xml file (SFDX metadata format). The optional webapplication.json file provides dev configuration.
flowchart TD
Start["sf ui-bundle dev"] --> CheckInside{"Inside webui/<br/>webapp folder?"}
CheckInside -->|Yes| HasNameInside{"--name provided?"}
HasNameInside -->|Yes, different| ErrorConflict["Error: --name conflicts<br/>with current directory"]
HasNameInside -->|No or same| AutoSelect["Auto-select current webapp"]
CheckInside -->|No| CheckSFDX{"In SFDX project?<br/>(sfdx-project.json)"}
CheckSFDX -->|Yes| CheckPath["Check force-app/main/<br/>default/webui/"]
CheckPath --> HasName{"--name provided?"}
CheckSFDX -->|No| CheckMetaXml{"Current dir has<br/>.webapplication-meta.xml?"}
CheckMetaXml -->|Yes| UseStandalone["Use current dir as webapp"]
CheckMetaXml -->|No| ErrorNone["Error: No webapp found"]
HasName -->|Yes| SearchByName["Find webapp by name"]
HasName -->|No| Prompt["Interactive selection prompt<br/>(always, even if 1 webapp)"]
SearchByName --> UseWebapp["Use webapp"]
AutoSelect --> UseWebapp
UseStandalone --> UseWebapp
Prompt --> UseWebapp
UseWebapp --> StartDev["Start dev server and proxy"]
| Scenario | Behavior |
|---|---|
--name myApp provided |
Finds webapp by name, starts dev server |
| Running from inside webapp folder | Auto-selects that webapp |
--name conflicts with current dir |
Error: must match current webapp or run from project root |
| At SFDX project root | Always prompts for webapp selection |
| Outside SFDX project with meta.xml | Uses current directory as standalone webapp |
| No webapp found | Shows error with helpful message |
my-sfdx-project/
├── sfdx-project.json # SFDX project marker
└── force-app/main/default/
└── webui/ # Standard SFDX location
├── app-one/ # Webapp 1 (with dev config)
│ ├── app-one.webapplication-meta.xml # Required: identifies as webapp
│ ├── webapplication.json # Optional: dev configuration
│ ├── package.json
│ └── src/
└── app-two/ # Webapp 2 (no dev config)
├── app-two.webapplication-meta.xml # Required
├── package.json
└── src/
The command uses a simplified, deterministic approach:
- Inside webapp folder: If running from
webui/<webapp>/or deeper, auto-selects that webapp - SFDX project root: Uses fixed path
force-app/main/default/webui/ - Standalone: If current directory has a
.webapplication-meta.xmlfile, uses it directly
Important: Only directories containing a {name}.webapplication-meta.xml file are recognized as valid webapps.
When multiple webapps are found, you'll see an interactive prompt:
Found 3 webapps in project
? Select the webapp to run: (Use arrow keys)
❯ app-one (webui/app-one)
app-two (webui/app-two) [no manifest]
app-three (webui/app-three)
Format:
- With manifest:
folder-name (path) - No manifest:
folder-name (path) [no manifest]
┌─────────────────────────────────────────────────┐
│ Your Browser │
│ http://localhost:4545 │
└───────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Proxy Server (Port 4545) │
│ │
│ Routes requests based on URL pattern: │
│ • /services/* → Salesforce (with auth) │
│ • Everything else → Dev Server │
└─────────┬─────────────────────┬─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌────────────────────────┐
│ Dev Server │ │ Salesforce Instance │
│ (localhost:5173)│ │ + Auth Headers Added │
│ React/Vue/etc │ │ + API Calls │
└─────────────────┘ └────────────────────────┘
Static assets (JS, CSS, HTML, images):
Browser → Proxy → Dev Server → Response
Salesforce API calls (/services/*):
Browser → Proxy → [Auth Headers Injected] → Salesforce → Response
The command operates in two distinct modes based on configuration:
| Mode | Configuration | Behavior |
|---|---|---|
| Command mode | dev.command is set (or default npm run dev) |
CLI starts the dev server. URL defaults to http://localhost:5173. Override with dev.url or --url if your dev server uses a different port. |
| URL-only mode | dev.url or --url only (no dev.command) |
CLI assumes the dev server is already running. Does not start the dev server. Starts proxy only and forwards to the given URL. |
URL precedence: --url flag > dev.url in manifest > default http://localhost:5173 (when command is used)
The webapplication.json file is optional. All fields are also optional - missing fields use defaults.
| Field | Type | Description | Default |
|---|---|---|---|
dev.command |
string | Command to start the dev server (e.g., npm run dev). When set, the CLI starts the dev server and uses default URL http://localhost:5173 unless overridden. |
npm run dev |
dev.url |
string | Dev server URL. Command mode: Override the default 5173 port if needed. URL-only mode: Required—the CLI assumes the server is already running and does not start it. | http://localhost:5173 |
Command mode (CLI starts dev server):
{
"dev": {
"command": "npm run dev"
}
}- CLI runs
npm run devand waits for the server to be ready - Default URL:
http://localhost:5173 - Override port: add
"url": "http://localhost:3000"if your dev server uses a different port
URL-only mode (proxy only, server already running):
{
"dev": {
"url": "http://localhost:5173"
}
}- No
dev.command— CLI does not start the dev server - You must start the dev server yourself (e.g.,
npm run devin another terminal) - CLI starts only the proxy and forwards to the given URL
No manifest (uses defaults):
- Dev command:
npm run dev - Default URL:
http://localhost:5173 - Manifest watching: disabled
{
"routing": {
"rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }],
"redirects": [{ "route": "/old-path", "target": "/new-path", "statusCode": 301 }],
"trailingSlash": "never",
"fallback": "/index.html"
}
}webui/
└── my-dashboard/
├── package.json # Has "scripts": { "dev": "vite" }
└── src/
Run: sf ui-bundle dev --target-org myOrg
Console output:
Warning: No webapplication.json found for webapp "my-dashboard"
Location: my-dashboard
Using defaults:
→ Name: "my-dashboard" (derived from folder)
→ Command: "npm run dev"
→ Manifest watching: disabled
💡 To customize, create a webapplication.json file in your webapp directory.
✅ Using webapp: my-dashboard (webui/my-dashboard)
✅ Ready for development!
→ Proxy: http://localhost:4545 (open this in your browser)
→ Dev server: http://localhost:5173
Press Ctrl+C to stop
{
"dev": {
"command": "npm run dev"
},
"routing": {
"rewrites": [{ "route": "/api/:path*", "target": "/services/apexrest/:path*" }],
"trailingSlash": "never"
}
}Edit webapplication.json while running - changes apply automatically:
# Console output when you change webapplication.json:
Manifest changed detected
✓ Manifest reloaded successfully
Dev server URL updated to: http://localhost:5174Note: Manifest watching is only enabled when
webapplication.jsonexists. Webapps without manifests don't have this feature.
The proxy continuously monitors dev server availability:
- Displays "No Dev Server Detected" page when server is down
- Auto-refreshes when server comes back up
- Shows helpful suggestions for common issues
Full Hot Module Replacement support through the proxy:
- Vite HMR (
/@vite/*,/__vite_hmr) - Webpack HMR (
/__webpack_hmr) - Works with React Fast Refresh, Vue HMR, etc.
Automatically detects Salesforce Code Builder environment and binds to 0.0.0.0 for proper port forwarding in cloud environments.
The --url flag overrides the dev server URL. Behavior depends on whether you have a command configured:
| Scenario | Command in manifest? | --url behavior |
|---|---|---|
| URL-only mode | No | Required. CLI assumes the server is already running and does not start it. Use when you run the dev server yourself. |
| Command mode | Yes | Optional override. Default is http://localhost:5173. Use --url to point to a different port. |
| URL reachable | Either | Proxy-only: skips starting dev server, starts proxy only |
| URL not reachable | Yes (command) | Starts dev server and warns if actual URL differs from --url |
| URL not reachable | No (URL-only) | Error: server must be running at the given URL |
When you run the dev server yourself:
# Terminal 1: Start your dev server manually
cd my-webapp
npm run dev
# Output: Local: http://localhost:5173/
# Terminal 2: Connect proxy to your running server
sf ui-bundle dev --url http://localhost:5173 --target-org myOrgOutput:
✅ URL http://localhost:5173 is already available, skipping dev server startup (proxy-only mode)
✅ Ready for development!
→ http://localhost:4545 (open this URL in your browser)
When using dev.command, the default URL is http://localhost:5173. Override with --url if your dev server uses a different port:
sf ui-bundle dev --url http://localhost:3000 --target-org myOrgIf the URL is not reachable, the CLI starts the dev server and uses the actual URL (with a warning if it differs).
Ensure your webapp has the required .webapplication-meta.xml file:
force-app/main/default/webui/
└── my-app/
├── my-app.webapplication-meta.xml # Required!
├── package.json
└── webapplication.json # Optional (for dev config)
The .webapplication-meta.xml file identifies a valid SFDX webapp. Without it, the directory is ignored.
This error occurs when you're inside one webapp folder but try to run a different webapp:
# You're in FirstWebApp folder but trying to run SecondWebApp
cd webui/FirstWebApp
sf ui-bundle dev --name SecondWebApp --target-org myOrg # Error!Solutions:
- Remove
--nameto use the current webapp - Navigate to the project root and use
--name - Navigate to the correct webapp folder
The --name flag matches the folder name of the webapp.
# This looks for webapp named "myApp"
sf ui-bundle dev --name myApp --target-org myOrgInstall dependencies in your webapp folder:
cd webui/my-app
npm install- Ensure dev server is running:
npm run dev - Verify URL in
webapplication.jsonis correct - Try explicit URL:
sf ui-bundle dev --url http://localhost:5173 --target-org myOrg
# Use a different port
sf ui-bundle dev --port 8080 --target-org myOrg
# Or find and kill the process using the port
lsof -i :4545
kill -9 <PID>Re-authorize your Salesforce org:
sf org login web --alias myOrgEnable detailed logging by setting SF_LOG_LEVEL=debug. Debug logs are written to the SF CLI log file (not stdout).
Step 1: Start log tail in Terminal 1
# Tail today's log file, filtering for webapp messages
tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev
# Or for cleaner output (requires jq):
tail -f ~/.sf/sf-$(date +%Y-%m-%d).log | grep --line-buffered WebappDev | jq -r '.msg'Step 2: Run command in Terminal 2
SF_LOG_LEVEL=debug sf ui-bundle dev --target-org myOrgExample debug output:
Discovering webapplication.json manifest(s)...
Using webapp: myApp at webui/my-app
Manifest loaded: myApp
Starting dev server with command: npm run dev
Dev server ready at: http://localhost:5173/
Using authentication for org: user@example.com
Starting proxy server on port 4545...
Proxy server running on http://localhost:4545
The command integrates with the Salesforce VSCode UI Preview extension (salesforcedx-vscode-ui-preview):
- Extension detects
webapplication.jsonin workspace - User clicks "Preview" button on the file
- Extension executes:
sf ui-bundle dev --target-org <org> --open - If multiple webapps exist, uses
--nameto specify which one - Browser opens with the app running
For scripting and CI/CD, use the --json flag:
sf ui-bundle dev --target-org myOrg --jsonOutput:
{
"status": 0,
"result": {
"url": "http://localhost:4545",
"devServerUrl": "http://localhost:5173"
}
}cd /path/to/plugin-ui-bundle-dev
# Install dependencies
yarn install
# Build
yarn build
# Link to SF CLI
sf plugins link .
# Verify installation
sf pluginsyarn build # Rebuild - no re-linking neededplugin-ui-bundle-dev/
├── src/
│ ├── commands/webapp/
│ │ └── dev.ts # Main command implementation
│ ├── auth/
│ │ └── org.ts # Salesforce authentication
│ ├── config/
│ │ ├── manifest.ts # Manifest type definitions
│ │ ├── ManifestWatcher.ts # File watching and hot reload
│ │ ├── webappDiscovery.ts # Auto-discovery logic
│ │ └── types.ts # Shared TypeScript types
│ ├── proxy/
│ │ ├── ProxyServer.ts # HTTP/WebSocket proxy server
│ │ ├── handler.ts # Request routing and forwarding
│ │ └── routing.ts # URL pattern matching
│ ├── server/
│ │ └── DevServerManager.ts # Dev server process management
│ ├── error/
│ │ ├── ErrorHandler.ts # Error creation utilities
│ │ ├── DevServerErrorParser.ts
│ │ └── ErrorPageRenderer.ts
│ └── templates/
│ └── error-page.html # Error page template
├── messages/
│ └── webapp.dev.md # CLI messages and help text
└── schemas/
└── webapp-dev.json # JSON schema for output
| Component | Purpose |
|---|---|
dev.ts |
Command orchestration and lifecycle |
webappDiscovery.ts |
SFDX project detection and webapp discovery |
org.ts |
Salesforce authentication token management |
ProxyServer.ts |
HTTP proxy with WebSocket support |
handler.ts |
Request routing to dev server or Salesforce |
DevServerManager.ts |
Dev server process spawning and monitoring |
ManifestWatcher.ts |
webapplication.json file watching for hot reload |
ErrorPageRenderer.ts |
Browser error page generation |
Repository: github.com/salesforcecli/plugin-ui-bundle-dev