Architecture

Two independent auth planes

The design keeps who you are and what the bridge may do in Microsoft's cloud completely separate:

PlaneWhoHow
Operator plane Your technicians using the SPA + API OIDC (JWT bearer) against your own identity provider, or self-registered local accounts with passkeys/TOTP and per-tenant sharing -- no IdP required. See Authentication for the full local-account model. The operator's name from the token is what lands in workflow run history either way.
Microsoft plane The bridge acting in customer tenants A multi-tenant Entra app under the Secure Application Model. SamTokenService exchanges the stored (encrypted, auto-rotated) SAM refresh token for a per-tenant Graph token on demand, scoped by that customer's GDAP roles.

Module layout

web/ (React+Vite+TS)  -->  src/PartnerCenterBridge.Api  -->  Core (contracts / desired state / reconcile / workflows)
                                    |- Graph          (tenant client factory, Intune Win32, users, Identity workflows)
                                    |- Exchange       (EXO PowerShell V3 out-of-process, Mailbox workflows)
                                    |- PartnerCenter  (SamTokenService, PartnerCenterClient)
                                    |- Data           (EF Core + Postgres, encrypted secrets, run history)
ProjectResponsibility
Core Domain entities, reconcile engine, the IWorkflow contract and catalog. No external SDK dependencies - everything else depends on Core, never the reverse.
Data EF Core BridgeDbContext + migrations. Secrets (like the SAM token) are encrypted at rest via ASP.NET Data Protection. Workflow runs and deployments live here.
PartnerCenter SamTokenService (MSAL SAM flow + token rotation), Partner Center REST client (customer list).
Graph Per-tenant Graph clients, the full Intune Win32 upload state machine, hire/offboard user service, and the Identity workflows.
Exchange What Graph can't do: mailbox configuration through the Exchange Online PowerShell V3 module (app-only certificate), run out-of-process, plus the Mailbox workflows.
Api Controllers, OIDC auth, DI wiring, deploy + provisioning orchestration, workflow dispatch + run recording.
web/ React SPA: Tenants, Contracts, App Templates, Deploy, History, New Hire, Offboard, Workflows.

The contract model

A contract is a named desired state shared by a set of tenants: which Win32 apps they should have (at which version), and how new users are provisioned. The reconcile engine compares desired state against what each tenant actually has and produces a plan - deploy, update, or nothing - that the operator reviews before it runs. Pushing a new template version fans the update out to every tenant on the contract.

The workflow engine

Workflows are the other half: not desired state, but known fixes for the problems that land in MSP queues every week. Each implements one interface:

public interface IWorkflow
{
    string Id { get; }           // "mfa-reset"
    string Name { get; }
    string Description { get; }
    string Category { get; }     // "Identity", "Mailbox"
    IReadOnlyList<WorkflowInput> Inputs { get; }   // rendered generically by the UI

    Task<DiagnosisResult> DiagnoseAsync(Tenant tenant, IReadOnlyDictionary<string,string> inputs, CancellationToken ct);
    Task<WorkflowRunResult> RemediateAsync(Tenant tenant, IReadOnlyDictionary<string,string> inputs, CancellationToken ct);
}

Implementations live in the backend project that owns the API they call (Graph or Exchange) and register through DI. The API lists and dispatches them uniformly and records every run; the UI renders each workflow's input schema. Adding a workflow requires zero controller or frontend changes. See Workflows for the catalog.

Audit trail

Every diagnose and remediate run is persisted: workflow, tenant, operator identity, inputs, every finding, every step, outcome, duration. Failures can additionally push to a webhook (Teams Adaptive Card or plain JSON - Notifications config section). Show-once secrets like generated temporary passwords are deliberately excluded from both.