Skip to content

Architecture

fabric-emulator is a local Microsoft Fabric runtime: the control-plane contract (workspaces, items, RBAC, git, jobs, LROs, Fabric Core MCP), a real OneLake ADLS/Blob data plane, and attached engines for Spark, T-SQL, pipelines, Airflow, KQL, and Eventstream. It is the sibling of entra-emulator: where that project is an Entra ID STS, this one is the Fabric surface that consumes Entra tokens.

Fabric is evergreen SaaS: no product version exists, and MicrosoftDocs/fabric-docs has no tags or releases (a continuously published main, ~100+ commits/week). The only contract version Microsoft exposes is the /v1 path segment of the REST API — that is what this emulator targets.

For clean-room reproducibility we therefore pin the docs commit, not a version. All claims in this doc set were verified against:

MicrosoftDocs/fabric-docs @ 0d63906ac29d8e8befa42b13f3d1d31c0f92081a (2026-07-10)

When re-auditing, diff the grounding files against this SHA (git diff 0d63906a.. -- docs/onelake/onelake-api-parity.md docs/cicd/git-integration/git-automation.md docs/security/workspace-identity.md docs/security/permission-model.md) and bump the pin.

A Fabric environment is four independent Azure products with four protocols. Each stays a separate emulator, reached only over HTTP, because that is what the real boundary is — and it is what lets any one of them be swapped for the live service unchanged (see 21-real-fabric-toggle.md):

  1. Entra ID — issues tokens (service-principal client credentials for the Fabric audience; workspace identities = auto-managed app registrations + service principals). Emulated by entra-emulator.
  2. Azure Key Vault — the secrets data plane. Fabric never holds the secret behind an Azure Key Vault reference connection: it stores a pointer and resolves it at use with a vault-audience token. Same for notebookutils.credentials.getSecret() inside a notebook. Emulated by azure-keyvault-emulator; fabric-emulator is a client of it (internal/akv).
  3. Azure Resource Manager — the resource lifecycle. A Fabric capacity is an ARM resource (Microsoft.Fabric/capacities), created and deleted through management.azure.com, not through the Fabric REST API. Emulated by arm-emulator; fabric-emulator reads from it (below).
  4. The Fabric control planehttps://api.fabric.microsoft.com/v1/…: workspace RBAC, item CRUD, item definitions (the CI/CD source format), git integration, deployment pipelines, long-running operations, and the workspace-identity lifecycle orchestration. Plus OneLake (https://onelake.dfs.fabric.microsoft.com), an ADLS-Gen2-shaped data plane. Emulated by fabric-emulator.

Keeping these as four composable emulators preserves single responsibility, and the token flow is one-way: entra ISSUES; fabric, keyvault and arm only VALIDATE, each against entra-emulator’s JWKS, exactly as the real products validate against Entra. No validator can mint a token.

ARM capacities, on by default. FABRIC_ARM_URL defaults to the arm-emulator service in docker-compose.yml, so a capacity created with PUT …/Microsoft.Fabric/capacities/{name} appears on GET /v1/capacities under the Fabric REST GUID ARM assigned at create. This process polls GET /_family/capacities (the same localhost family channel the Key Vault sibling uses for authorization); the seeded capacity stays alongside whatever ARM contributes. Setting FABRIC_ARM_URL= empty opts out and leaves the seed only, which is how the hermetic test paths run.

Every core service in docker-compose.yml appears in that diagram, and scripts/check_arch_services.py fails the build if one stops appearing. The check exists because this document described a two-system model for as long as Key Vault had been a default service — the shape was a decision and stayed true, but the cast of services is a list, and lists drift.

fabric-emulator deliberately reuses entra-emulator’s stack and idioms so the two form a coherent pair and the testing primitives carry over:

ConcernChoice (same as entra-emulator)
Language / HTTPGo, stdlib net/http, host-routed muxes
Storagemodernc.org/sqlite (pure-Go, no CGO)
Surface routingHost-header router (onelake.* data plane; everything else = control plane + portal)
DeterminismControllable clock (drives LRO completion) + fault injection
PortalSvelte 5, go:embed all:dist, committed dist + CI drift guard
Docs siteAstro Starlight on GitHub Pages, pinned, /docs = source of truth
DistributionGoReleaser: binaries, distroless Docker (GHCR), Homebrew, winget
TestsGo unit/integration + real-SDK e2e matrix + Playwright mount smoke
LicenseApache-2.0, clean-room from fabric-docs

The payoff of reuse: entra-emulator’s deterministic clock becomes Fabric’s LRO controller — a test can force any async operation to Succeeded instantly or hold it Running to exercise polling. Real Fabric cannot do this.

Every /v1/… and OneLake request carries Authorization: Bearer <jwt>. fabric-emulator validates:

  • Signature against entra-emulator’s JWKS ({issuer}/discovery/v2.0/keys), fetched once and cached, keyed by kid.
  • Issuer equals the configured entra-emulator issuer.
  • Audience ∈ {https://api.fabric.microsoft.com, https://analysis.windows.net/powerbi/api} for the control plane; the Storage audience (https://storage.azure.com) for OneLake.
  • Expiry / nbf via the standard claim checks.

It then maps the token’s oid/appid/sub to a workspace role for RBAC. This mirrors entra-emulator’s own ValidateAccessToken model; the only new work is the audience set and the role lookup. Config: --entra-issuer + --entra-jwks-url (or a discovery URL). It can point at a real tenant unchanged.

Host muxServes
api.fabric.microsoft.comthe /v1 control plane (workspaces, items, RBAC, git, LRO, jobs, admin) + the Livy/Spark data plane (…/livyapi/…, high-concurrency sessions)
onelake.dfs.fabric.microsoft.comADLS-Gen2 (DFS) subset (filesystem = workspace, path = item/…)
onelake.blob.fabric.microsoft.comthe OneLake Blob dialect (Put Blob/Block, If-None-Match:* Delta put-if-absent) — what delta-rs / object_store use
a raw TCP/TDS listener (--sql-tds-addr)the warehouse T-SQL over TDS endpoint — Entra FedAuth terminated, session spliced to a SQL Server sidecar (internal/tds)
any other host, at /the Svelte operator portal (embedded; reads state via /_emulator/portal/*)

See 07-control-plane-api.md for the endpoint catalog and wire shapes, and 13-roadmap.md for what lands in each phase.

One pure-Go SQLite database holds the entire state — workspaces, items and their verbatim definition parts, RBAC, capacities, operations, jobs, git remotes, OneLake blobs, and the workspace-identity link — with cascading deletes matching the control plane’s semantics. The full schema, seed, and state enums live in 06-data-model-and-seed.md.

Nearly every mutation returns 202 Accepted with an x-ms-operation-id header (what the documented automation scripts read), a Location: /v1/operations/{id}, and Retry-After. Clients poll GET /v1/operations/{id} until Status leaves {NotStarted, Running}. The emulator models this as a first- class operation row whose completeAt is a function of the controllable clock:

  • default: completes on the next poll (fast, deterministic for CI);
  • --lro-delay or per-request fault: stays Running for N virtual seconds;
  • fault injection: forces Failed with a Fabric-shaped error body.

Capacity/SKU billing, Power BI semantic-model evaluation, Purview audit, and real network/firewall enforcement. Emulating engine internals or KQL execution is also out of scope.

Not a non-goal (any more): real compute. The core Go binary stays a contract emulator, but real engines attach as opt-in sidecars — so notebooks and pipelines actually run (Spark-compatible Sail compute via a Livy agent), T-SQL runs for real over TDS against a SQL Server sidecar, and Delta lands in OneLake — and where no real engine can be attached, the surface returns an honest 501 rather than faking a result. The principle is never fake compute, not no compute. See 14-real-compute.md.

  • Depends on entra-emulator only over HTTP (JWKS + issuer; plus a token-mint call for workspace identities — the shipped identity handshake). No shared process.
  • Depends on azure-keyvault-emulator only over HTTP too, and only as an outbound client: internal/akv GETs {vaultURI}/secrets/{name} with a vault-audience bearer when a credential’s keyReference (or sibling) is resolved. The secret value is returned to the caller and never persisted in the emulator’s database — only the {vaultUri, secretName} pointer is — which is the property that makes credential-by-reference worth having.
  • There is no global vault setting to point elsewhere: each connection carries its own vaultUri, as in real Fabric, so a connection naming a real vault works alongside one naming the emulator with no reconfiguration.
  • For Go integration tests, it may import entra-emulator’s public emulator package to run both in one process with no network — an ergonomics option, not a coupling requirement.
  • Primary local composition: a sibling docker-compose.yml brings up both, fabric pre-wired to entra’s issuer/JWKS. An auto-loaded docker-compose.override.yml additionally attaches real engines (Spark agent, SQL Server) by default — see 14-real-compute.md.