Install
$ agentstack add skill-gusbavia-fabric-apps-skills-fabric-app-lakehouse-live ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Fabric App · Lakehouse Live Bridge
The browser acquires a delegated Fabric token (the signed-in user's own identity) and POSTs straight to a Lakehouse "API for GraphQL" endpoint. Live data, per-user permissions and RLS, zero infrastructure, no secrets in the bundle.
browser → MSAL (Entra SSO, delegated) → POST → GraphQL API over the Lakehouse
A working reference implementation exists in the fabric-apps-starter repo, template-lakehouse-live/ (Vite + React + MSAL, query console + token inspector).
The recipe — order matters, each step gates the next
1. Validate CORS before writing code. Preflight the GraphQL endpoint with the app origin:
curl -X OPTIONS -H "Origin: https://" \
-H "Access-Control-Request-Method: POST" -i
# want: 200 + Access-Control-Allow-Origin echoing your origin
Fabric allows Fabric Apps origins by default. If this fails, nothing downstream can work.
2. MSAL singleton, initialized BEFORE render. When the browser returns from login.microsoftonline.com, the SPA boots again and MSAL must parse the auth response on that load:
// lib/msal.ts
export const msal = new PublicClientApplication({
auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`,
redirectUri: window.location.origin },
cache: { cacheLocation: "sessionStorage" },
});
export async function bootstrapMsal() {
await msal.initialize();
await msal.handleRedirectPromise(); // skip this → sign-in silently never lands
}
// main.tsx: bootstrapMsal() runs before createRoot(...).render(...)
Prefer loginRedirect over popups for SPAs: no blocked windows, no blank popup pages, works embedded and standalone.
3. App registration (Entra portal):
- Authentication → add platform → Single-page application → redirect URI = the app URL (plus
http://localhost:5173for dev). Authorization Code + PKCE; leave both implicit-grant checkboxes UNchecked. - API permissions → Power BI Service → Delegated →
GraphQLApi.Execute.All→ grant admin consent. This is THE permission that gates query execution;Tenant/Workspace/Dataset/Item.Read.Allall return 403 without it.
4. Token + query:
let tok;
try {
tok = await msal.acquireTokenSilent({
account, scopes: ["https://api.fabric.microsoft.com/.default"],
});
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
tok = await msal.acquireTokenPopup({ account, scopes: [...] }); // fallback
} else throw e;
}
const res = await fetch(GRAPHQL_ENDPOINT, {
method: "POST",
headers: { Authorization: `Bearer ${tok.accessToken}`,
"Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
5. Workspace access: the signed-in user needs access to the GraphQL API item (direct "Run Queries and Mutations" grant, or a workspace role).
Query shape (Fabric GraphQL over a Lakehouse)
Types are pluralized, fields snake_case, rows under items:
{ dim_employees(first: 500) { items { employee_id employee_name } } }
Introspection is usually disabled — get the schema from the GraphQL item's editor in the portal, not from an introspection query.
Always set first explicitly and size it generously. Omitted or small first caps the result and sorted reads drop the tail with NO error. Raising first well past the table size (e.g. first: 50000 on a 5k-row table) is the proven simple fix; after any data growth, verify returned row counts against the source.
Debug pattern: decode the token on screen
A 403 is almost always a missing scope. Decode the JWT payload and surface aud + scp in the UI; the diagnosis becomes one glance:
JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
// want: aud = https://api.fabric.microsoft.com, scp includes GraphQLApi.Execute.All
Gotchas
| Symptom | Cause / fix | |---|---| | Sign-in completes but no session | handleRedirectPromise() not called before render. | | AADSTS50011 redirect URI error | App URL not registered as an SPA platform (not "Web"). | | 403 InsufficientPrivileges with a valid token | Missing delegated GraphQLApi.Execute.All or missing admin consent. If the decoded scp ALREADY shows the scope, the cause is item access instead — grant the user "Run Queries and Mutations" on the GraphQL API item or a workspace role. | | Fixed the permission, still 403 | The cached token predates the consent. Sign out (or clear sessionStorage) and sign in again so the fresh token carries the new scp. | | Rows silently missing at scale | first: N caps the result; sorted reads drop the tail QUIETLY. Raise first or paginate — verify row counts against the source. | | Saved data takes minutes-to-a-day to appear | Lakehouse SQL endpoint syncs lazily. This path is read-only by design; write-back belongs in fabric-app-sqldb-writeback. |
When NOT to use
Any transactional write-back. The endpoint is read-only, has no mutations, and its lazy sync makes "write somewhere else, read it back here" feel broken to users.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: gusbavia
- Source: gusbavia/fabric-apps-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.