Docs Get started

Local Workbench

Download and double-click PartnerCenterBridge.exe to open the Windows desktop workbench. Add Microsoft 365 tenants with their admin accounts, then manage people and run operations from one window. Your workbench data stays under your Windows user profile; no server or external identity provider needs to be set up first. The desktop app uses the same workflow and operation code as the server edition.

First run, click by click

Start the exe, and it opens a desktop window containing the workbench once its local API is healthy. The API chooses an available loopback port automatically. The Windows WebView2 Runtime is required; the window explains how to install it if it is missing. A brand-new install has no accounts yet. The first screen asks you to create an administrator account, or choose Skip — use without an account on this computer. There is no multi-step setup wizard to click through first.

The account you register here becomes the instance Administrator (skipping is covered below). Sign in afterward (a passkey once you've registered one, or your password) and Home is where you land: a person search, the four most common actions, and a Finish setting up card that only lists what still needs attention -- it disappears once every check is clean.

Each open check links straight to the screen that fixes it -- the same checks doctor and /settings/workbench report -- or, for the Exchange Online module, offers an Install action on Windows and keeps the exact command available to copy. Installation requires an instance Administrator's confirmation. If you choose Not now, the check remains visibly blocked across restarts; a successful install refreshes the dependency check and makes Exchange features available without restarting the app. Nothing here is guessed at: a missing GDAP relationship, an unbootstrapped SAM connection, or a missing PowerShell module all show up by name, with their fix, instead of a generic "not configured."

Once a tenant is connected, every planned operation (mirroring access, offboarding) and every known fix ends at the same evidence view, whether you just ran it or you're opening it later from Activity or a person's History tab:

See Operations for the full walkthrough of mirroring access and offboarding, and below for what the first account and first launch actually do server-side.

Running it

PartnerCenterBridge.exe [command] [options]

Commands:
  (none)          Open the desktop window (API + UI in one process).
  doctor          Check configuration and dependencies, print the results and exit
                  (exit code 0 = no errors, 1 = at least one error). Does not start the server.
  bootstrap-sam   Run the interactive Secure Application Model bootstrap (device code) and exit.

Options:
  --local             Use the Local Workbench profile. The Local Workbench build uses it by
                      default, so you will not normally need to pass it.
  --browser           Open the classic browser and tray host instead of the desktop window.
  --port <N>          CLI/browser host port (default 5080).
  --data-dir <path>   Data directory (default %LOCALAPPDATA%\PartnerCenterBridge).
  --listen <address>  Bind to another address instead of 127.0.0.1. Exposes the app to the
                      network; only use it when you understand the consequences.
  --no-browser        Run the local API without a browser or desktop window. A workbench used without an
                      account prints its sign-in link instead: that link grants full
                      access to this workbench, so do not share it.
  --version           Print the version and exit.
  -h, --help          Print this help and exit.

Any --Section:Key=value argument is passed through as configuration.

The executable is a Windows GUI application. For PowerShell scripts that need to wait and read its exit code, use Start-Process .\PartnerCenterBridge.exe -ArgumentList 'doctor' -NoNewWindow -Wait -PassThru. Capture diagnostics with -RedirectStandardOutput and -RedirectStandardError.

On first launch the process creates its data directory, opens (and migrates) the SQLite database, waits for its own /health endpoint to answer, and then loads the app in WebView2. Close the window to stop it. CLI mode prints the URL and data path to the terminal.

Browser and tray mode

Run PartnerCenterBridge.exe --browser to use the browser-hosted mode. The workbench keeps running in the Windows notification area after you close its browser tab. Double-click its bridge icon to open a fresh workbench window. Right-click for Open workbench, Open logs, or Exit PartnerCenterBridge. An Explorer-launched console is detached after the tray icon is ready; an existing terminal is left intact. Use --no-browser or --Hosting:Tray=false for console operation.

Add tenants with Microsoft sign-in

In a loopback-only Local Workbench, an operator with tenant registry permission can choose Settings > Microsoft connections > Add tenant with Microsoft (also available on Tenants). The system browser opens Microsoft's account picker. Sign in with an administrator of the organization, complete MFA and consent, and the tenant appears using its directory name and verified domains. Repeat for each tenant. Existing tenants require Owner access in the workbench before an admin connection can be attached.

Technicians do not need to register an application in every customer tenant. Production builds include PartnerCenterBridge's publisher-owned public client ID; each customer tenant decides whether to grant consent. This is the same principle used by Microsoft Graph PowerShell's default application registration. The publisher registration is bundled in the Windows build; no technician app setup is required.

For distributors: use a custom Microsoft application

The official download already includes the publisher registration. These instructions are only for a custom distribution. Register an application in Microsoft Entra with accounts in any organizational directory. Add the Mobile and desktop applications platform with http://localhost as a redirect URI. Configure delegated Microsoft Graph permissions: Organization.Read.All is required to discover the tenant; add only the delegated permissions needed by your chosen operations and obtain tenant admin consent. The desktop flow uses authorization code with PKCE and requires no client secret. Supply the public ID with ./scripts/publish-local.ps1 -MicrosoftClientId YOUR-APPLICATION-ID, or set PCB_MICROSOFT_CLIENT_ID for local builds. For releases, set the release environment variable MICROSOFT_SIGN_IN_CLIENT_ID. Release packaging requires it.

An Administrator may optionally supply a custom app under Advanced: use your own Microsoft application in Microsoft connections. It is saved to protected local storage and takes effect immediately. Existing MicrosoftSignIn:ClientId configuration remains supported. The app ID cannot be switched while account connections exist, because their OAuth caches belong to the original app.

The application requests the registration's Graph .default permissions. A successful sign-in does not grant roles or bypass tenant policy: Graph actions require both the delegated permission and the signed-in administrator's appropriate role. Guest administration should reconnect an already registered target tenant to select its tenant authority.

MSAL silently renews tokens as needed. Each local operator and tenant has a separate encrypted cache that survives restarts; neither tokens nor refresh tokens are sent to the SPA. When Microsoft requires interaction, the connection shows Reconnect with its remembered username. Reconnection targets the same tenant and account, including any claims challenge. Cancellation or signing in as a different account leaves the saved connection intact.

Direct sign-in covers Microsoft Graph. Partner Center still uses SAM/GDAP, and Exchange Online still uses its app-only certificate. A direct-only tenant never silently borrows the partner's identity. Sharing a tenant does not share the admin token cache. Unattended jobs without a local operator identity cannot use these direct connections. Server-hosted interactive sign-in is not included in this release.

See Microsoft's silent token acquisition guidance for why consent changes, revocation, and Conditional Access can require another sign-in.

Where data lives

Everything is under one data root: %LOCALAPPDATA%\PartnerCenterBridge on Windows ($XDG_DATA_HOME/PartnerCenterBridge, falling back to ~/.local/share/PartnerCenterBridge, on other platforms the same binary could run on), or wherever --data-dir points instead.

PathContents
pcb.dbSQLite database (tenants, contracts, workflow/operation runs, accounts, ...).
keys\ASP.NET Data Protection key ring -- DPAPI-protected on Windows. Encrypts the SAM refresh token and the local signing key.
auth-signing-key.protectedThe generated Auth:Local:SigningKey, itself protected with the key ring above.
logs\Rolling file logs.
packages\Uploaded .intunewin app packages.
certs\Where an Exchange Online app-only certificate (exo.pfx) is expected if you configure Exchange.
pcb.local.jsonOptional hand-edited config overrides, in ordinary appsettings JSON shape.

First launch and the first administrator

A fresh install has Auth:Mode=Local and no registered accounts. The first account you register becomes the instance Administrator -- there is no separate setup wizard. GET /api/system/status (anonymous, nothing sensitive) reports needsFirstUser so the SPA can send you straight to registration instead of a sign-in form that would otherwise fail with nothing to sign in to. A Local first run requires the single-use setup link the exe opens (or prints with --no-browser); it expires after 30 minutes. Open the exe again if the link has expired. The Local profile refuses to start at all with Auth:Mode=Dev -- that mode has no credentials, which combined with a bound network port would let anyone who can reach it act as administrator.

Using it without an account

Choosing Skip — use without an account on this computer on the first screen is an explicit choice meant for a machine that is already secured: screen lock, disk encryption, and no shared Windows account. It is not a localhost bypass. This creates a built-in workbench owner with instance Administrator rights and nothing else -- like any other account, it only gets access to a tenant (as that tenant's Owner) once it adds or syncs that tenant.

  • The desktop window receives a single-use launch link minted by the running process. The ticket expires after two minutes and is consumed when used. Running PartnerCenterBridge.exe again requests a fresh link over a Windows named pipe restricted to your user; the second process verifies the running process's owner before opening that link in browser mode. Normal desktop launches focus the existing window. A copied or bookmarked link cannot be reused.
  • With --no-browser, the launch link is printed to this console window instead of being opened -- never written to the log file, so it never lands in logs\.
  • Opening a bookmark later does not sign you in (there is no sign-in form to use without an account). It tells you to start the app from PartnerCenterBridge.exe again.
  • Self-registration, password login, passkeys, and TOTP two-factor are all blocked for this account while it has no password: POST /api/auth/register refuses a second account, and adding a passkey or enabling TOTP is refused with a message pointing at Settings > Account & security > Protect with an account. MCP access tokens are not blocked -- you can create one for a scripted client the same as any account.
  • Protect with an account, from Settings > Account & security, turns this into an ordinary Local account (email + password): outstanding launch tickets and earlier sessions and MCP tokens are revoked, and passkeys, TOTP, and further registrations become available. Tenants, settings, and history are untouched.
  • It exists only in the Local Workbench. It is never available in the Server profile (Docker or Kubernetes), and first run refuses to offer it -- not just "turn it off" -- when the exe is started with a non-loopback --listen address.

Second launch and port collisions

Before binding its port, the Local profile checks both loopback addresses. If the port is in use, the exe requests a fresh link through a current-user-only Windows named pipe and verifies the server process belongs to your Windows user. A verified workbench opens the new link and the second process exits. If verification fails, the exe reports a port collision without opening a URL or exposing a ticket. The default listener holds both 127.0.0.1 and [::1] when IPv6 is available, so another process cannot shadow localhost on IPv6.

The canonical origin

Passkeys are bound to one origin, so the Local profile has exactly one: http://localhost:<port>. A request addressed to a loopback IP literal (127.0.0.1 or [::1]) is redirected there for GET/HEAD, or rejected with 421 Misdirected Request for anything else -- silently replaying a request body at a different origin is not something a client should rely on. This only normalizes which host a request is answered on; it grants nothing, and no request is trusted for being local (see below). See Authentication for how this interacts with WebAuthn's relying-party id.

Diagnostics: doctor and /api/system/diagnostics

doctor runs the same checks as GET /api/system/diagnostics (full detail there too -- whoever can run the binary already has its configuration) and prints each one with a suggested fix command and/or an SPA route to open, then exits 1 if any check is an error. A signed-in caller who is not an instance Administrator gets only the capability flags (graph, exchange, partnerCenter), not the check detail.

Check idWhat it verifies
hostingProfile and version; warns if bound to a non-loopback address.
databaseCan connect; applied vs. pending migrations.
data-protectionKey ring folder exists, is writable, and holds at least one key.
authLocal: user/administrator counts. Dev: warns it is not for shared use. Oidc: authority configured.
samPartner app configured, and whether SAM has been bootstrapped or a seed token is set.
tenantsAt least one active tenant onboarded.
pwshPowerShell 7 found on PATH (or Exchange:PwshPath).
exchange-moduleThe ExchangeOnlineManagement module installed for that pwsh.
exchange-appExchange app id configured and the app-only certificate present -- in the Windows certificate store (Exchange:CertificateThumbprint) or on disk (Exchange:CertificatePath).

Exchange Online as an optional dependency

Mailbox operations (mailbox archive repair, offboarding's mailbox conversion and forwarding) need three things that nothing else in the app requires: pwsh on PATH or at a standard Windows installation location, the ExchangeOnlineManagement module installed for it, and an app-only certificate. On Windows, prefer Exchange:CertificateThumbprint: a certificate already imported into Cert:\CurrentUser\My (or, failing that, Cert:\LocalMachine\My) with its private key, so no certificate secret is ever handled by the app at all. Otherwise, set Exchange:CertificatePath (defaults to certs\exo.pfx under the data root) and, if the PFX is password-protected, Exchange:CertificatePassword -- that password reaches pwsh only over standard input, never written to a temporary file or passed on the command line. Everything else works without any of it. When a piece is missing, the affected section reports Unavailable with the specific reason instead of a silent no-op -- the person workspace's mailbox section, and any offboarding item that needs Exchange. doctor and the diagnostics endpoint probe pwsh and the module out-of-process (cached 60 seconds so repeated checks stay fast) and the certificate fresh every time, and report the first missing piece in the order you'd actually fix them.

The Exchange organization to connect to is read from Microsoft Graph for that tenant (its verified initial *.onmicrosoft.com domain, cross-checked against the tenant's own Entra id) the first time an Exchange operation runs against it, then cached on the tenant -- never guessed from the tenant's display domain. If the connection ever answers for a different organization than expected, the operation refuses to run and nothing is changed.

Protecting secrets and data

  • DPAPI-protected key ring -- the Data Protection key ring under keys\ is encrypted at rest for the current Windows user (ProtectKeysWithDpapi), in addition to ordinary file-system persistence. It protects the SAM refresh token and the local signing key. Losing it, or moving the data directory to another user account, makes those secrets undecipherable -- the sam and data-protection checks say so rather than failing silently.
  • Protected signing key -- Auth:Local:SigningKey is generated once (256 random bits) and stored encrypted with that same key ring at auth-signing-key.protected. Every restart reuses it; deleting the file (or the key ring) rotates it and signs everyone out.
  • User-only ACLs -- a data directory this process creates (not one you point --data-dir at) gets its Windows ACL restricted to the current user plus SYSTEM, best-effort. A failure to restrict it is a startup warning, not a hard failure, since %LOCALAPPDATA% is already per-user.
  • Defense in depth on the network side -- loopback-only Kestrel binding, the canonical-origin middleware, and AllowedHosts pinned to exactly localhost;127.0.0.1;[::1] (plus an explicit --listen address, when given) all apply together, so a DNS-rebinding host cannot reach the app even if it tricked a browser into resolving a public name to loopback.
  • No localhost auth bypass -- none of the above is authentication. Loopback binding and the canonical-origin check only normalize which origin a request lands on; every request still has to be signed in, either through the same Auth:Mode=Local sign-in as any other deployment or, if you chose to use it without an account, through the one-time launch link tied to your Windows user. There is no special-cased "requests from 127.0.0.1 are trusted" path.

Building it

./scripts/publish-local.ps1 (.NET 8 SDK + Node.js/npm) builds the SPA (npm ci + npm run build, unless -SkipSpaBuild is given, in which case an existing web/dist is embedded as-is) and publishes a single-file, self-contained, trimmed-symbols exe to artifacts/local/win-x64/PartnerCenterBridge.exe. Equivalent command:

dotnet publish src/PartnerCenterBridge.Desktop -c Release -r win-x64 --self-contained `
  -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true `
  -p:EnableCompressionInSingleFile=true -p:DebugType=embedded -p:PcbLocalWorkbench=true -p:PcbDesktop=true `
  -o artifacts/local/win-x64

.github/workflows/ci.yml runs this on every PR and push to main (Windows runner), then smoke-tests the published exe: health check, the /api/system/status profile, the SPA fallback for a deep link, loopback-only binding, and a stop/restart to confirm pcb.db persists. A separate GUI smoke test checks that the desktop window starts its API and creates a WebView2 profile. It also runs doctor non-gating (Exchange and SAM are expected unconfigured in CI) and uploads the exe as a 14-day build artifact.