DEV Community

Cover image for Fabric Apps: Row-Level Security — The Silent Hero
Przemysław Kujda
Przemysław Kujda

Posted on

Fabric Apps: Row-Level Security — The Silent Hero

Row-Level Security: The Silent Hero of Fabric Apps

In my first article, I walked through the "hybrid architecture" for Fabric Apps: a custom React frontend layered on top of native Semantic Models and Power BI visuals, with an AI assistant added on top. The response was great, but it surfaced the one question every enterprise architect asks the moment they see a slick demo: "Okay, but is it secure?"

Here's the uncomfortable truth: a beautiful React dashboard is worthless if it fails a security audit. You can nail every animation, every micro-interaction, every pixel, and still get shut down in week one of a compliance review because a regional sales rep can query global revenue numbers.

This is where Row-Level Security (RLS) comes in. It doesn't get a keynote slot, it doesn't trend on social media, but it's the thing that actually makes a Fabric App enterprise-ready. Let's talk about why.

The Nightmare of Custom Web App Security (The "Old Way")

If you've ever built a custom analytics app from scratch (Node backend, React frontend, some flavor of SQL warehouse), you know the drill. Security isn't a feature, it's a project unto itself:

  • You build a user-mapping table that ties application logins to business dimensions (region, department, cost center).
  • You write middleware that intercepts every query and injects a WHERE clause based on that mapping.
  • You maintain a token/session layer that has to stay perfectly in sync with your identity provider.
  • You duplicate this filtering logic in every downstream service that touches the data: API, exports, scheduled jobs, the AI chatbot someone added in month six.

The real risk isn't that this is hard to build once. It's that it's hard to keep consistent. Every time someone adds a new endpoint or a new report, they have to remember to re-implement the filter. Miss one spot (a debug endpoint, a CSV export, a cached query) and you've got a data leak. Security logic that lives in application code is security logic that will drift from the source of truth.

The Fabric App Paradigm Shift (Entra ID & Semantic Models)

Fabric Apps flip this model entirely. Instead of building authorization from scratch, you inherit it.

  • Identity is native. The app runs inside the Microsoft ecosystem, so every user is already a known identity in Microsoft Entra ID. No shadow user table, no custom login flow to maintain.
  • The frontend stays dumb (on purpose). The React layer doesn't carry any filtering logic. It simply passes the authenticated user's context along when it queries the Semantic Model via Fabric REST APIs, or hits a Lakehouse SQL endpoint directly.
  • The engine does the enforcing. RLS rules defined once in the Semantic Model are applied server-side, before a single row of data leaves Fabric. The frontend never sees data it isn't entitled to, so there's nothing to accidentally expose.

This gives you what I'd call "one version of the truth, one version of security." The same RLS rule that governs a native Power BI report also governs your custom React dashboard, your export job, and your AI assistant. No duplication, no drift.

A conceptual look at what this means on the frontend. Note there's no filtering logic here, just identity propagation:

// Conceptual: querying a Semantic Model via Fabric REST API
// In a real React app, you'd get the userToken via @azure/msal-react
// The frontend never constructs a WHERE clause, it just forwards identity.

async function queryModel(daxQuery: string, userToken: string) {
  const response = await fetch(
    `https://api.fabric.microsoft.com/v1/workspaces/${workspaceId}/semanticModels/${modelId}/query`,
    {
      method: "POST",
      headers: {
        // Entra ID token identifies the user to the engine.
        // RLS is resolved server-side based on this identity.
        Authorization: `Bearer ${userToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ query: daxQuery }),
    }
  );

  // Whatever comes back is ALREADY row-level filtered.
  // The React component just renders it.
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Compare that to the "old way" snippet you'd write for a custom backend: a buildFilterClause(user.region, user.department) helper that you'd have to call in every single query path. That function simply doesn't exist here. It can't drift because it was never duplicated in the first place.

Why This Is a Game-Changer for AI Copilots

This matters even more once you bring an AI assistant into the picture, which, if you followed the first article, you already have.

Text-to-SQL and natural-language Q&A features are notoriously risky from a security standpoint. An LLM doesn't inherently know that "show me the highest-paid employee" should be scoped to the analyst's own department. If your AI layer talks directly to raw tables and then summarizes whatever comes back, you've built a very articulate data leak.

With native RLS, the sequencing changes completely:

  1. The AI assistant sends a query (or a generated DAX/SQL statement) to the Semantic Model on behalf of the authenticated user.
  2. The Fabric engine applies RLS first, filtering the result set down to only what that user is entitled to see.
  3. Only the already-filtered, already-safe aggregate is handed to the LLM (Gemini, OpenAI, whatever you're using) for summarization into natural language.

The LLM never touches unfiltered data. It can't leak the CEO's salary to a curious analyst because it never received it in the first place. The security boundary sits below the AI layer, not inside a prompt or a system message you're hoping the model respects. That's a structurally stronger guarantee than anything you'd get from prompt engineering alone.

Conclusion & Call to Action

Put together, this is the real pitch for Fabric Apps: you get to spend your engineering time on product UX (animations, interactions, the AI assistant, the things that make an app feel good to use) without reinventing authorization, and without praying that every query path respects the same filtering rule.

The frontend stays thin. The security model stays centralized. And your audit conversation gets a lot shorter.

I'd love to hear how the community is handling this in other embedded or custom-app scenarios. Are you relying on native RLS, or still bridging gaps with custom middleware? Drop your approach in the comments.

Code for this series (evolving as I go): https://github.com/przemq99/first-fabric-app

Top comments (0)