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 policy | Row-level security enforced on every query |
| auth.fabric.enabled: true | Entra ID (Fabric SSO) sign-in |
| staticHosting config | A 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 gettingstartedauthThis 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.htmlStep 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 devThe 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.