Programming model overview

Rayfin SDK uses a decorator-driven programming model where you define your data schema once in TypeScript and automatically receive production-ready APIs, type-safe clients, and infrastructure.

Key concepts

Rayfin SDK combines three core elements:

  • Decorator-driven schema: Use TypeScript decorators to define data models, permissions, and relationships.
  • Automatic API generation: Your decorated classes become GraphQL endpoints without writing controller code.
  • Type-safe clients: Generated TypeScript clients provide compile-time validation for queries and mutations.

How it works

When you create a Fabric app, your code flows through these stages:

# Stage What happens
1 Developer You author the app in your editor of choice.
2 TypeScript You write entity classes in TypeScript.
3 Decorators You annotate classes and fields with @entity, @uuid, @text, @role, and other decorators.
4 Schema The CLI compiles the decorated classes into a database schema, permission policies, and API configuration.
5 APIs The schema is published as GraphQL endpoints.
6 Client The generated RayfinClient exposes a type-safe data and auth client over those endpoints.
7 Application Your frontend application consumes the client to read and write data.

1. Define data models with decorators

You define your data structure using TypeScript classes and decorators from @microsoft/rayfin-core:

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

@entity()
export class Product {
  @uuid() id!: string;
  @text() name!: string;
  @text({ optional: true }) description?: string;
  @int() price!: number;
}

2. Schema generation

The Rayfin CLI (npx rayfin) analyzes your decorated classes and generates:

  • Database schema - Tables, columns, constraints, and indexes
  • API configuration - GraphQL endpoint definitions
  • Permission policies - Row-level security and field-level access control

4. Type-safe client usage

GraphQL APIs are available to perform CRUD operations on your database. Rayfin SDK provides a data client operation out of the box to read or write or delete data.

import { RayfinClient } from '@microsoft/rayfin-client';

const client = new RayfinClient();

// TypeScript knows about Product fields
const products = await client.data.products.query()
  .select(['id', 'name', 'price'])
  .execute();

// Compile-time error if field doesn't exist
const invalid = await client.data.products.query()
  .select(['nonexistentField'])  // ❌ TypeScript error
  .execute();

Decorator reference

Rayfin SDK provides decorators for common data modeling patterns:

Entity decorators

Decorator Purpose Example
@entity() Mark a class as a database entity @entity() class Product

Property decorators

Decorator Database type TypeScript type
@uuid() UNIQUEIDENTIFIER string
@text() NVARCHAR string
@int() INT number
@decimal() DECIMAL number
@bool() BIT boolean
@date() DATETIME2 Date

Permission decorators

Decorator Purpose
@role() Define role-based permissions

See Define data permissions for authorization details.

Functions

Create one UserDataFunctions instance, and register each function by calling udf.func(name, handler, connections?).

Follow these rules when you define a function:

  • Place the RayfinContext parameter anywhere in the handler signature. The runtime identifies it by type, injects it, and excludes it from the generated function inputs.
  • Declare a connection for each delegated token the function needs. Omit the connections array for functions that only access app data or don't need delegated tokens.

The following example registers a plain function and a function that uses the invocation context:

import {
  UserDataFunctions,
  type RayfinContext,
} from '@microsoft/fabric-user-data-functions';

// Match rayfin/data/schema.ts so getDataClient() is fully typed.
import type { TripPlan } from '../../data/TripPlan.js';

type AppSchema = { TripPlan: TripPlan };

const udf = new UserDataFunctions();

udf.func('greet', async (name: string): Promise<string> => {
  return `Hello, ${name}!`;
});

udf.func(
  'whoAmI',
  async (ctx: RayfinContext<AppSchema>): Promise<string> => {
    return ctx.accessToken ? 'authenticated' : 'anonymous';
  },
);

The RayfinContext provides the function's runtime services:

Member Purpose
ctx.accessToken Provides the signed-in user's Rayfin JSON Web Token (JWT). Decode the sub claim to get a stable user ID.
ctx.Tokens.<Audience> Gets a platform-provided resource token for an audience declared in the RayfinContext annotation. Deployed Functions use the app identity and its permissions. For details, see Connect Functions to external resources.
ctx.getSecret('NAME') Gets a secret for the current invocation. Returns undefined when the secret isn't set.
ctx.getDataClient() Gets the typed data API for your entities. The client supports create, findMany, update, delete, and upsert operations.
ctx.baseUrl Provides the Rayfin endpoint for the current item.
ctx.publishableKey Provides the publishable key for the current item.

For setup, local debugging, and deployment instructions, see Use Functions in Fabric Apps.

Development workflow

A typical development cycle follows this pattern:

  1. Define or modify data models - Add or update TypeScript classes with decorators
  2. Test locally with remote backend - Run npm run dev to test your frontend code changes against your App backend in Fabric.
  3. Deploy to Fabric - Run npx rayfin up to deploy to the managed Fabric service and applies your schema changes.

Changes to your TypeScript models automatically propagate through the entire stack — from database schema to API endpoints to client types.

Authorization

Permissions are defined alongside your data models using the @role decorator:

@entity()
@role('authenticated', ['create', 'read', 'update', 'delete'], {
  policy: (claims, item) => claims.sub.eq(item.userId)
})
export class UserDocument {
  @uuid() id!: string;
  @text() userId!: string;
  @text() content!: string;
}

This approach ensures:

  • Security rules live next to the data they protect
  • Type-safe policy expressions catch errors at compile time
  • Refactoring entity fields automatically updates permission checks