Blog Post

Fabric platform Community Blog
6 MIN READ

Building and Deploying a Full-Stack App with Microsoft Fabric Apps

knaveen's avatar
knaveen
Icon for Microsoft Employee rankMicrosoft Employee
4 months ago

What is Fabric Apps?

Fabric Apps (Preview) lets you build and host full-stack applications directly on Microsoft Fabric. Instead of standing up your own database, API, identity provider, and hosting, you describe your data model in TypeScript and the platform provisions everything for you:

 

You write

Fabric provisions
A class with field decorators (@entity, , @uuid...)A SQL database + SQL analytics endpoint
A @role policyRow-level security enforced on every query
auth.fabric.enabled: trueEntra ID (Fabric SSO) sign-in
staticHosting configA globally hosted web app with HTTPS

npx rayfin up

A managed App item in your workspace

 

The result is a single deployable unit that shows up as items in your Fabric workspace: no separate cloud subscription, no infrastructure to manage.

What we'll build

The app we build is PartFlow, a manufacturing part-ordering and supplier-management tool. Every screenshot in this guide is from a real deployment to the Apps Fabric workspace: github.com/navintkr/fabric-apps

Each person sees only their own suppliers and orders, backed by a real SQL database, enforced by row-level security.

Prerequisites

  • Node.js 18+ and npm (this demo I used Node v22.19.0 / npm 10.9.3)
  • A Microsoft Fabric workspace on a supported capacity (I used capacity demofabriccentralus, F8, Central US) with the Fabric Apps preview enabled
  • The Rayfin CLI, run on demand via npx Microsoft/rayfin-cli@latest
  • A code editor: VS Code with GitHub Copilot is ideal

Step 1: Scaffold the project

Create a new Fabric App from the built-in authenticated template:

npx @Microsoft/rayfin-cli@latest init --template gettingstartedauth

This generates a React + TypeScript (Vite) front end already wired to the Rayfin SDK, plus a rayfin/ folder that holds your backend definition.

Step 2: Define your data model

The entire backend is described with TypeScript decorators. PartFlow uses two entities. First, the Supplier:

// rayfin/data/Supplier.ts

import { entity, role, text, email, set, int, date, uuid } from '@microsoft/rayfin-core';

@entity()
@role('authenticated', '*', {
  // Row-level security: a user can only touch rows where user_id == their token's "sub"
  policy: (claims, item) => claims.sub.eq(item.user_id),
})
export class Supplier {
  @uuid() id!: string;
  @text({ min: 1, max: 100 }) name!: string;
  @email() contactEmail!: string;
  @set({ enum: ['raw-materials', 'components', 'electronics', 'tooling', 'packaging'] })
  category!: 'raw-materials' | 'components' | 'electronics' | 'tooling' | 'packaging';
  @INT({ min: 1, max: 365 }) leadTimeDays!: number;
  @date() createdAt!: Date;
  @text() user_id!: string;
}

 

Then the PartOrder, which carries a status workflow and a money field:

//rayfin/data/PartOrder.ts

import { entity, role, text, set, int, decimal, date, uuid } from '@microsoft/rayfin-core';

@entity()
@role('authenticated', '*', {
  policy: (claims, item) => claims.sub.eq(item.user_id),
})
export class PartOrder {
  @uuid() id!: string;
  @text({ min: 1, max: 50 }) partNumber!: string;
  @text({ min: 1, max: 100 }) partName!: string;
  @text({ min: 1, max: 100 }) supplierName!: string;
  @INT({ min: 1, max: 100000 }) quantity!: number;
  @decimal({ precision: 12, scale: 2 }) unitPrice!: number;
  @set({ enum: ['pending', 'approved', 'shipped', 'received', 'cancelled'] })
  status!: 'pending' | 'approved' | 'shipped' | 'received' | 'cancelled';
  @date() orderedAt!: Date;
  @text() user_id!: string;
}

 

What each piece does:

  • @entity() creates a table (Supplier, PartOrder) and a GraphQL CRUD API for it.
  • @uuid / text/ email/ set/ INT/ @decimal / dateare strongly-typed columns. Validation (like min/max, the email format, or the set enum) is enforced server-side.
  • @role('authenticated', '*', ...) means only signed-in users can read/write, and the policy clause guarantees each user sees only their own rows. This is real row-level security, not just a UI filter.

 

Both classes are exported from rayfin/data/schema.ts so the client gets full type safety:

//rayfin/data/schema.ts

import { PartOrder } from './PartOrder.js';
import { Supplier } from './Supplier.js';

export type ManufacturingSchema = { Supplier: typeof Supplier; PartOrder: typeof PartOrder };
export const schema = [Supplier, PartOrder];

Step 3: Configure services

rayfin/rayfin.yml turns platform capabilities on or off. For this demo we enabled auth, data, and static hosting:

//rayfin/rayfin.yml

id: fabric-apps-demo
name: fabric-apps-demo
version: 1.0.0
services:
  auth:
    enabled: true
    fabric:
      enabled: true        # Entra ID / Fabric SSO in production
    password:
      enabled: true        # Email + password for local dev
    allowedRedirectUris:
      - http://localhost:5173
      - https://<your-app>.webapp.fabricapps.net
    scopes:
      - read:data
      - write:data
  data:
    enabled: true
    dialect: mssql         # Provisions a Fabric SQL database
  staticHosting:
    enabled: true
    folder: dist
    buildCommand: npm run build:fabric
    indexDocument: index.html

Step 4: Use the typed client in your app

The SDK gives you a fully-typed, fluent data API: no hand-written SQL or REST calls. The service layer reads the signed-in user from the session and lets row-level security do the rest:

// src/services/rayfin/RayfinPartOrderService.ts
const client = getRayfinClient();

// READ: RLS automatically scopes results to the current user
const orders = await client.data.PartOrder
  .select(['id', 'partNumber', 'partName', 'supplierName', 'quantity', 'unitPrice', 'status', 'orderedAt'])
  .orderBy({ orderedAt: 'desc' })
  .execute();

// CREATE: stamp the row with the authenticated user's id
const user_id = client.auth.getSession().user?.id;
await client.data.PartOrder.create({
  partNumber,
  partName,
  supplierName,
  quantity,
  unitPrice,
  status: 'pending',
  orderedAt: new Date(),
  user_id,
});

// UPDATE (advance the workflow) / DELETE
await client.data.PartOrder.update({ id }, { status: 'approved' });
await client.data.PartOrder.delete({ id });

The Supplier service follows the same pattern, and useSuppliers seeds three sample suppliers on first load so a fresh sign-in isn't an empty screen.

 

Step 5: Run locally

npm run dev

The dev script provisions the backend services and starts Vite. Locally you can use the email + password flow (no Entra tenant required), so the inner loop stays fast.

Step 6: Deploy to Fabric

One command builds the front end, provisions the SQL database, applies the schema, deploys the static content, and registers the App item in your workspace:

npx @Microsoft/rayfin-cli@latest up --workspace Apps --yes

 

Highlights from the deploy output:

  • Schema applied: Supplier: 7 fields, PartOrder: 9 fields, each with 1 row-level-security role
  • Static content deployed: 5 files (~689 KB) to global hosting
  • Publishable key issued: used by the client SDK
  • Live URL: https://<name>-centralus.webapp.fabricapps.net

Step 7: Manage the app in the Fabric portal

After deploy, the app appears as a managed item in your workspace. A single rayfin up created three items in the Apps workspace: the App itself plus the SQL database and SQL analytics endpoint that back it.

 

 

The SQL database and its analytics endpoint are fully-fledged Fabric items. You can open the SQL editor in the portal to query the Supplier and PartOrder tables directly, wire them into Power BI through the analytics endpoint, or govern them like any other Fabric data item.

Step 8: Sign in with Microsoft (Fabric SSO)

Because auth.fabric.enabled is true, the deployed app presents an Entra ID sign-in. No auth code to write, no secrets to manage:

 

After a single click, the user is authenticated and lands on the procurement dashboard. The email shown in the header is the real signed-in identity, and every supplier and order below is served from the live SQL database:

 

Step 9: Create data (full round-trip)

Raising a part order writes through the typed client to the GraphQL API to the Fabric SQL database, with row-level security stamping it to the current user. The order starts in pending and can be advanced through the workflow with a click:

This is the complete loop: React UI to Rayfin SDK to GraphQL API to SQL database and back to UI, all authenticated and scoped per user.

Step 10: Evolve the schema (code-first migrations)

Need new columns or a whole new table? Edit the entities and re-apply. PartFlow itself started life as a single-table demo and grew into the two-entity supplier + order model shown above, with no manual ALTER TABLE and no downtime.

Apply schema changes to the live database with one command:

npx @Microsoft/rayfin-cli@latest up db apply --workspace Apps

 

The CLI compiles your TypeScript, diffs the model against the deployed database, and migrates it:

TypeScript compilation successful
   Found @entity class: Supplier
   Supplier: 7 fields, 1 permission roles
   Found @entity class: PartOrder
   PartOrder: 9 fields, 1 permission roles
Configuration applied successfully!

 

Destructive changes: dropping or replacing a table (for example, swapping one data model for another) can cause data loss. The CLI refuses such migrations unless you pass --force: rayfin up db apply --force.

 

That's it. We have now built and deployed live to the Apps Fabric workspace.

Fabric Apps is in preview, with features and APIs evolving, and Fabric data connections, storage, real-time services, and native agent support are on the roadmap.

Updated 4 months ago
Version 1.0