---
name: fabric-api
description: "Use when calling the Microsoft Fabric REST API from a Rayfin app — control plane workflows, workspace/item listing, on-demand pipeline runs, notebook or Spark job submission, job polling, or any browser-side Fabric admin UI. Triggers: Fabric API, api.fabric.microsoft.com, MSAL, @azure/msal-browser, PublicClientApplication, ssoSilent, acquireTokenSilent, acquireTokenRedirect, handleRedirectPromise, loginHint, Entra SPA app registration, SPN, service principal, delegated permissions, Power BI Service permissions, Workspace.Read.All, Item.ReadWrite.All, Item.Execute.All, fabricToken, fabricApi, fabricFetch, setFabricToken, acquireFabricTokenSilent, handleMsalRedirect, listWorkspaces, listPipelines, runPipeline, getJobInstance, listJobInstances, cancelJobInstance, DataPipeline, jobType Pipeline, sparkjob, RunNotebook, control plane, Fabric admin UI, VITE_FABRIC_ENTRA_CLIENT_ID, VITE_FABRIC_ENTRA_TENANT_ID, allowedRedirectUris, Fabric iframe popup blocked"
metadata:
  author: becoleman
  version: 0.1.0
---
# Fabric API from Rayfin

Pattern for calling the Microsoft Fabric REST API (`https://api.fabric.microsoft.com/v1`) from a Rayfin-hosted SPA. Rayfin's auth controller does not issue Fabric-scoped tokens, so this skill layers `@azure/msal-browser` on top of the existing Rayfin session to acquire a delegated Fabric access token via an Entra SPA app registration. Reference implementation: <https://blog.nebloc.com/posts/rayfin_api/>.

## Reference Docs

Use the existing Rayfin tooling for Rayfin-specific concerns (auth session, `client.data.*`, deployment). For Fabric REST endpoints and required scopes, prefer the official Microsoft docs.

- Fabric REST API reference — <https://learn.microsoft.com/rest/api/fabric/articles/>
- Job Scheduler API (on-demand pipeline / notebook / sparkjob runs) — <https://learn.microsoft.com/rest/api/fabric/core/job-scheduler>
- MSAL.js browser docs — <https://learn.microsoft.com/entra/identity-platform/msal-js-overview>
- Use the `microsoft_docs_search` / `microsoft_docs_fetch` MCP tools for current endpoint payloads when the exact request shape is needed.

For Rayfin pieces (`useAuth`, `user.email`, deployment, `allowedRedirectUris`), defer to the `rayfin` skill.

## Rules

### Platform

- Two-token model: Rayfin issues the user's app session (`useAuth` / `user.email`); MSAL issues a separate Fabric-scoped access token used only for `api.fabric.microsoft.com` calls. Do not try to reuse the Rayfin session token against Fabric.
- Add `@azure/msal-browser` (>= `^5.14.0`) as a runtime dependency. No server-side MSAL — this is a pure SPA flow.
- Keep two files: `src/services/fabricToken.ts` (MSAL plumbing) and `src/services/fabricApi.ts` (typed REST wrapper). Pages consume both — never inline `fetch()` against `api.fabric.microsoft.com` in components.
- A single `PublicClientApplication` instance is cached at module scope and lazily initialized — call `await pca.initialize()` exactly once before any token request.
- Cache MSAL state in `localStorage` (`cache: { cacheLocation: 'localStorage' }`) so silent SSO survives reloads.

### Security

- The SPA app registration is a separate Entra app from anything Rayfin manages. Configure it with platform = Single-page application and redirect URIs for **every** host the app runs on: local dev (`http://localhost:5173`), the Fabric apps hosting URL (e.g. `https://<slug>-<region>.webapp.fabricapps.net`), and any preview URLs.
- Use **delegated** permissions only. The Fabric API surfaces these under `Power BI Service` in the Entra portal → API permissions. Do not use application permissions / client secrets — this is user-delegated auth.
- Only request scopes the page actually needs. Typical control-plane scope set: `Workspace.Read.All`, `Item.ReadWrite.All`, `Item.Execute.All`. Add more (`Workspace.ReadWrite.All`, `Capacity.Read.All`, etc.) only when a specific call requires them.
- Tenant ID and SPA client ID are **public** values and safe to commit. Surface them via `VITE_FABRIC_ENTRA_CLIENT_ID` / `VITE_FABRIC_ENTRA_TENANT_ID` in `.env` (committed). Validate both with a small `readClientConfig()` helper that throws a clear error when either is missing — fail fast at module load, not deep inside an MSAL call.
- Never store the Fabric access token in `localStorage`, cookies, or React state that gets serialized. Hold it in a module-scoped variable inside `fabricApi.ts` (set via `setFabricToken`) and clear it on sign-out via `clearFabricToken`.
- Pass the Rayfin user's email as the MSAL `loginHint` so SSO targets the same identity — never prompt the user to pick a different account.
- Keep the SPA app registration's redirect URI list tightly scoped. Wildcards are not supported and not safe.

### Token Acquisition

- Set `redirectUri: window.location.origin` in the `PublicClientApplication` config so MSAL matches whichever registered URI corresponds to the current host (local dev, Fabric apps URL, preview). Do not hardcode a single redirect URI in code — let `window.location.origin` resolve it at runtime.
- Acquisition fallback order is fixed: `ssoSilent` → `acquireTokenSilent` (cached account matched by `loginHint`) → `acquireTokenRedirect`. Do not use `acquireTokenPopup` — Fabric hosts the app in an iframe that blocks popups.
- `acquireTokenRedirect` navigates the page away. The function throws `'Redirecting...'` after calling it; callers must check for that message and treat it as a benign no-op (the page is unloading).
- Call `handleMsalRedirect()` (which wraps `pca.handleRedirectPromise()`) **once on mount** of any page that uses Fabric, before any silent acquisition. If it returns a token, use it directly and skip the silent path.
- Always wait for `user?.email` from Rayfin before kicking off MSAL — `loginHint` is required for the silent SSO path to succeed inside the Fabric iframe.
- Wrap the token-acquisition `useEffect` with a `cancelled` flag to avoid setting state after unmount.
- After acquiring the token, hand it to `setFabricToken(token)` before triggering any data load.

### REST Wrapper

- All requests go through one `fabricFetch<T>(path, options?)` helper that prefixes `FABRIC_API_BASE`, sets `Authorization: Bearer ${token}` and `Content-Type: application/json`, and handles status codes uniformly.
- Throw `Fabric API <status>: <body>` on non-2xx — surface the response body so Entra/Fabric error codes are visible.
- Treat `202 Accepted` as `{ status: 'accepted', location: res.headers.get('Location') }`. The Job Scheduler returns 202 with a `Location` header pointing at the new job instance — do **not** call `res.json()` on it.
- Treat `204 No Content` as `undefined` — endpoints like `cancel` return no body.
- `encodeURIComponent` every workspace ID, item ID, and job instance ID interpolated into the path.
- Use the Items API `type` query filter (e.g. `/items?type=DataPipeline`) instead of listing everything and filtering client-side.
- Job Scheduler path segments are case-sensitive: `Pipeline` for Data Pipelines, `RunNotebook` for notebooks, `sparkjob` for Spark Job Definitions. Encode these as constants, not magic strings at call sites.
- Poll job status with `GET /workspaces/{ws}/items/{item}/jobs/instances/{id}` — do not assume the 202 response body contains the job ID; parse it out of `Location` if you need it.

### Deployment

- Add the deployed Fabric apps hosting URL to the SPA app registration redirect URIs **before** the first deploy or the post-deploy MSAL redirect will fail with `AADSTS50011`.
- Fabric SSO (Entra ID) is only available inside the Fabric Portal. Local dev uses email/password Rayfin auth — the MSAL flow still works because `loginHint` is just an email string.
- After `rayfin up`, verify both: (1) the app loads at the new hosting URL, and (2) `acquireFabricTokenSilent` succeeds without a full-page redirect on the second visit.

## Anti-Patterns

- Never call `fetch('https://api.fabric.microsoft.com/...')` directly from a component or hook — always go through `fabricFetch` in `fabricApi.ts` so auth, error handling, and status-code conventions stay consistent.
- Never store the Fabric access token in React state, `localStorage`, or anywhere that survives serialization — hold it in module scope and clear it on sign-out.
- Never use `acquireTokenPopup` — popups are blocked inside the Fabric iframe. Use `acquireTokenRedirect` as the final fallback.
- Never start MSAL acquisition before `user?.email` from Rayfin is available — silent SSO without `loginHint` will fail inside the iframe.
- Never request broader Fabric scopes than the page needs (e.g. `Tenant.ReadWrite.All` for a workspace list) — the consent prompt will be harder to approve and you widen the blast radius if the token leaks.
- Never use application permissions or embed a client secret — this is a public SPA, the secret would be visible in the bundle.
- Never call `res.json()` on a 202 response from the Job Scheduler API — there is no JSON body, only a `Location` header.
- Never hardcode workspace IDs, item IDs, or capacity IDs in the REST wrapper — they are runtime inputs from `listWorkspaces` / `listPipelines`.

## File Layout

```text
src/
  services/
    fabricToken.ts   # MSAL singleton + acquireFabricTokenSilent + handleMsalRedirect
    fabricApi.ts     # FABRIC_API_BASE, fabricFetch, setFabricToken, hasFabricToken, listWorkspaces, listPipelines, runPipeline, ...
  pages/
    ControlPlanePage.tsx   # consumes both: useEffect to acquire token, then load data
.env                      # VITE_FABRIC_ENTRA_CLIENT_ID, VITE_FABRIC_ENTRA_TENANT_ID (public values)
```

## Quick Reference

```bash
# Install MSAL
npm install @azure/msal-browser
```

```ini
# .env — public values, safe to commit
VITE_FABRIC_ENTRA_CLIENT_ID=<spa-app-client-id>
VITE_FABRIC_ENTRA_TENANT_ID=<tenant-id>
```

```ts
// src/services/fabricToken.ts — token acquisition (ssoSilent → silent → redirect)
const FABRIC_SCOPES = [
  'https://api.fabric.microsoft.com/Workspace.Read.All',
  'https://api.fabric.microsoft.com/Item.ReadWrite.All',
  'https://api.fabric.microsoft.com/Item.Execute.All',
];

function readClientConfig(): { clientId: string; tenantId: string } {
  const clientId = import.meta.env.VITE_FABRIC_ENTRA_CLIENT_ID;
  const tenantId = import.meta.env.VITE_FABRIC_ENTRA_TENANT_ID;
  if (!clientId || !tenantId) {
    throw new Error('Missing Entra SPA app config. Set VITE_FABRIC_ENTRA_CLIENT_ID and VITE_FABRIC_ENTRA_TENANT_ID.');
  }
  return { clientId, tenantId };
}

// new PublicClientApplication({ auth: { clientId, authority, redirectUri: window.location.origin }, cache: { cacheLocation: 'localStorage' } })
export async function handleMsalRedirect(): Promise<string | null> { /* pca.handleRedirectPromise() */ }
export async function acquireFabricTokenSilent(loginHint: string): Promise<string> { /* fallback chain */ }
```

```ts
// src/services/fabricApi.ts — typed REST wrapper
const FABRIC_API_BASE = 'https://api.fabric.microsoft.com/v1';
const PIPELINE_JOB_TYPE = 'Pipeline';

export function setFabricToken(token: string): void;
export function clearFabricToken(): void;
export function hasFabricToken(): boolean;
export async function listWorkspaces(): Promise<FabricWorkspace[]>;
export async function listPipelines(workspaceId: string): Promise<FabricItem[]>;
export async function runPipeline(workspaceId: string, pipelineId: string, body?): Promise<RunPipelineResult>;
export async function getJobInstance(workspaceId: string, pipelineId: string, jobInstanceId: string): Promise<JobInstance>;
export async function cancelJobInstance(workspaceId: string, pipelineId: string, jobInstanceId: string): Promise<void>;
```

```tsx
// Page wiring — runs once when user.email becomes available
useEffect(() => {
  if (!user?.email) return;
  let cancelled = false;
  (async () => {
    try {
      const redirectToken = await handleMsalRedirect();
      if (cancelled) return;
      if (redirectToken) { setFabricToken(redirectToken); setTokenState('ready'); return; }
      const token = await acquireFabricTokenSilent(user.email);
      if (cancelled) return;
      setFabricToken(token);
      setTokenState('ready');
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (msg.includes('Redirecting')) return; // page is navigating away
      setTokenError(msg);
      setTokenState('error');
    }
  })();
  return () => { cancelled = true; };
}, [user?.email]);
```

## Common Endpoints

| Action | Method | Path | Required scope |
| --- | --- | --- | --- |
| List workspaces | `GET` | `/workspaces` | `Workspace.Read.All` |
| List Data Pipelines | `GET` | `/workspaces/{ws}/items?type=DataPipeline` | `Item.ReadWrite.All` |
| Run pipeline on demand | `POST` | `/workspaces/{ws}/items/{item}/jobs/Pipeline/instances` | `Item.Execute.All` |
| Run notebook on demand | `POST` | `/workspaces/{ws}/items/{item}/jobs/RunNotebook/instances` | `Item.Execute.All` |
| Run Spark Job Definition | `POST` | `/workspaces/{ws}/items/{item}/jobs/sparkjob/instances` | `Item.Execute.All` |
| List job instances | `GET` | `/workspaces/{ws}/items/{item}/jobs/instances` | `Item.ReadWrite.All` |
| Get job instance | `GET` | `/workspaces/{ws}/items/{item}/jobs/instances/{id}` | `Item.ReadWrite.All` |
| Cancel job instance | `POST` | `/workspaces/{ws}/items/{item}/jobs/instances/{id}/cancel` | `Item.Execute.All` |

## Troubleshooting

- `AADSTS50011: redirect URI mismatch` — the current `window.location.origin` is not in the SPA app registration's redirect URI list. Add it and re-test.
- `interaction_required` from `ssoSilent` — expected on first visit; the fallback chain handles it by calling `acquireTokenRedirect`.
- `Fabric API 401: ...` — token is missing or expired. Confirm `setFabricToken` ran before the call and that the scope set covers the endpoint.
- `Fabric API 403: ...` — scope is correct but the signed-in user lacks the Fabric role (e.g. not a workspace member). Fix in Fabric, not in code.
- Token acquisition loops on redirect — `handleMsalRedirect()` is not being called on mount, or it is being called *after* `acquireFabricTokenSilent`. Call it first.
- Popup blocked errors in dev tools while running inside Fabric — confirm no code path is calling `acquireTokenPopup`; only `acquireTokenRedirect` is supported inside the Fabric iframe.
