--- title: "Rayfin" description: "Define your data model in TypeScript. Rayfin generates the database and type-safe APIs, then runs them on Microsoft Fabric — with auth, functions, storage, and hosting built in." url: https://rayfin.ai/docs markdown_url: https://rayfin.ai/docs.md section: docs product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:46:21-07:00 source: index.mdx --- # Rayfin > Define your data model in TypeScript. Rayfin generates the database and type-safe APIs, then runs them on Microsoft Fabric — with auth, functions, storage, and hosting built in. Rayfin is a backend platform for TypeScript developers. You define your data model as decorated TypeScript classes, and Rayfin generates the database schema, REST and GraphQL APIs, and type-safe clients — then runs them on Microsoft Fabric with authentication, functions, blob storage, and static hosting built in. ```typescript title="rayfin/data/Todo.ts" import { entity, uuid, text, boolean, authenticated } from '@microsoft/rayfin-core'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id) }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @boolean({ default: false }) isCompleted!: boolean; @text({ max: 128 }) user_id!: string; } ``` That class becomes a table, a GraphQL API, a typed client, and a row-level security policy. One command deploys all of it. ```bash npx rayfin up ``` > [!NOTE] > Rayfin supports TypeScript as the only language for data models, client code, and > application logic. ## Get started [#get-started] If you read one page, read the [Quickstart](/docs/start/quickstart) — it goes from an empty terminal to a running app in four commands. * **[Installation](/docs/start/installation)** — install Node.js 20+ and the GitHub CLI on Windows, macOS, or Linux. * **[Quickstart](/docs/start/quickstart)** — scaffold a project, deploy its backend to Fabric, and run the frontend locally against it. * **[Project structure](/docs/start/project-structure)** — the `rayfin/` folder, `rayfin.yml`, and how entity classes become a database schema. * **[Deploy to Fabric](/docs/start/deploy-to-fabric)** — enable the tenant setting, create a Fabric app, and deploy with `rayfin login` and `rayfin up`. ## How a Fabric app runs [#how-a-fabric-app-runs] Rayfin runs one way: as a managed **Fabric app** on Microsoft Fabric. ```mermaid flowchart TB Dev(["Developer"]) -->|"rayfin up"| CLI["Rayfin CLI"] CLI ==>|"deploy"| Fabric User(["End user"]) ==> Static subgraph Fabric["Microsoft Fabric — inside your tenant"] Static["Static content"] Web["WebService"] DataApi["Data API Builder"] Fn["Functions"] Blob[("Blob storage")] MSSQL[("MSSQL")] end subgraph Outside["Outside the app"] Entra["Microsoft Entra ID"] MSRes["Microsoft resources
Fabric · Azure AI
Key Vault · Cosmos DB
Kusto · Azure DevOps"] Sources["Existing Fabric data
Warehouse · SQL DB
Semantic model · KQL"] AnyApi["Any HTTPS API"] Entra ~~~ MSRes ~~~ Sources ~~~ AnyApi end Static ==> Web Web ==> DataApi ==> MSSQL Web ==> Fn Fn ==> MSSQL Web ==> Blob Web -.->|"Fabric SSO"| Entra Web -.->|"connectors"| Sources Fn -.->|"delegated token"| MSRes Fn -.->|"secrets"| AnyApi class Dev,User actor class CLI,Static,Web,DataApi service class MSSQL store class Fn,Blob experimental class Entra,MSRes,Sources,AnyApi external ``` `rayfin up` packages your project and provisions it as a Fabric app: Fabric hosts the MSSQL database, a WebService and Data API Builder layer in front of it, your built frontend as static content, and sign-in through Fabric SSO (Entra ID) — the only auth provider available once the app is deployed. [Functions](/docs/functions) and [blob storage](/docs/storage) are optional services you enable in `rayfin.yml`. Functions are also how the app reaches anything outside itself. They call any HTTPS API using secrets from `rayfin secret set`, and they reach Microsoft resources — Fabric, Azure AI, Key Vault, Cosmos DB, Event Grid, Kusto, Azure DevOps — through [delegated authentication](/docs/functions/connections): the runtime exchanges the caller's identity for a resource-scoped token, so the function acts **as the signed-in user** rather than as a shared service identity. [Connectors](/docs/connectors) cover the other direction: data that already lives in Fabric. Point one at a Warehouse, SQL Database, Lakehouse SQL endpoint, semantic model, or KQL database and query it from the same client — as typed entities, or with DAX and KQL. Those queries also run under the signed-in user's identity. See [Delegated access](/docs/auth/delegated-access) for how the surfaces relate. > [!WARNING] > Functions, blob storage, connectors, and delegated authentication are experimental or in > preview, and are not available in every Fabric region or tenant. Confirm availability in > your tenant before you depend on them. During development, a local Vite server takes the place of the *Static content* node above: `npx rayfin up --exclude-services staticHosting` deploys everything else to Fabric, and `npm run dev` serves the frontend from `localhost` against that deployed backend. There is no backend to run yourself. ## Build your app [#build-your-app] * **[Data](/docs/data)** — model entities, query them, and control who can read each row. * **[Auth](/docs/auth)** — sign users in with Fabric SSO and read claims on the server. * **[Functions](/docs/functions)** — server-side TypeScript for logic that does not belong in the client. Experimental. * **[Connectors](/docs/connectors)** — read and write data that already exists in Fabric: warehouses, SQL databases, semantic models, and KQL databases. Preview. * **[Storage](/docs/storage)** — upload, serve, and secure files in blob storage. Experimental. * **[Hosting](/docs/hosting)** — build and serve your frontend from the deployed app. * **[Deploy](/docs/deploy)** — secrets, environments, capacity, and troubleshooting. ## Using an agent [#using-an-agent] These docs are built for coding agents as much as for people. Every page has a raw Markdown mirror — append `.md` to any URL — and the whole corpus is available at [`/llms-full.txt`](/llms-full.txt). ```prompt title="Point your agent at Rayfin" Read https://rayfin.ai/llms.txt and then https://rayfin.ai/docs/reference/agent-rules.md so you know how to work with the Rayfin SDK and CLI. Every page on that site is available as raw Markdown by appending .md to its URL. Then help me build a Rayfin app. ``` See [Rules for coding agents](/docs/reference/agent-rules) for the constraints an agent needs before it writes Rayfin code. ## Reference [#reference] * [CLI](/docs/reference/cli) — every command and flag. * [`rayfin.yml`](/docs/reference/config/rayfin-yml) — the configuration schema. * [SDK](/docs/reference/sdk) — API reference for every `@microsoft/rayfin-*` package. * [Known limitations](/docs/reference/known-limitations) — current constraints and workarounds. --- --- title: "Delegated access" description: "Understand how Fabric SSO, function connections, connectors, and entity permissions use the signed-in user's identity." url: https://rayfin.ai/docs/auth/delegated-access markdown_url: https://rayfin.ai/docs/auth/delegated-access.md section: auth product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: auth/delegated-access.mdx --- # Delegated access > Understand how Fabric SSO, function connections, connectors, and entity permissions use the signed-in user's identity. Use delegated access when server-side Rayfin code needs to reach a Microsoft resource without a long-lived credential. Rayfin starts from the [Fabric SSO](/docs/auth/fabric-sso) session, exchanges the signed-in user's identity for a resource-scoped token, and runs the server-side call **as the caller** rather than as a shared service account. Everything on this page builds on Fabric SSO. The user signs in once, Rayfin manages the session, and delegated surfaces use that caller identity when they reach functions, connectors, or Rayfin-owned data. > [!WARNING] > Function connections and connectors are preview features. Confirm they are available in > your tenant before you design a production workflow around them. ## Choose the delegated surface [#choose-the-delegated-surface] | Surface | You declare | You get | Runs as | | --------------------------------------------------- | --------------------------------------- | -------------------------------------------- | ----------------------------------------------------------- | | [Function connections](/docs/functions/connections) | `udf.connection({ audienceType })` | `ctx.getToken(AudienceType.X)` | The signed-in user | | [Connectors](/docs/connectors/auth) | `auth.type` in `rayfin.yml` | A typed client on `client.connectors.` | The signed-in user (`delegated`) or the app (`application`) | | [Your own data](/docs/data/permissions) | `@role()` / `@authenticated()` policies | `client.data.` | The signed-in user | ## Call Microsoft resources from functions [#call-microsoft-resources-from-functions] Use a [function connection](/docs/functions/connections) when custom server-side code needs to call a Microsoft resource. You declare `udf.connection({ audienceType })` on the function, then read the scoped token inside the handler with `ctx.getToken(AudienceType.X)`. The function still runs inside your deployed Fabric app. The token identifies the signed-in user, so the target resource decides whether that user can read or write. ## Query existing Fabric sources [#query-existing-fabric-sources] Use a [connector](/docs/connectors/auth) when you want to query an existing Fabric data source through `client.connectors.`. Category A connectors expose typed entities for Fabric SQL sources. Category B connectors expose DAX or KQL query operations. Connector auth is explicit in `rayfin.yml`. `auth.type: delegated` runs every query as the signed-in user. `auth.type: application` runs as the app identity and is available only for the Category A Fabric SQL connector types. ## Secure data Rayfin owns [#secure-data-rayfin-owns] Use [entity permissions](/docs/data/permissions) when the data lives in Rayfin-managed entities. `@role()` and `@authenticated()` policies compare the signed-in user's claims to fields on each row, so `client.data.` returns only rows the policy allows. ## Grant access at the source [#grant-access-at-the-source] Delegated auth passes the user's identity through; it does not grant access by itself. The signed-in user must already have permission on the target Fabric workspace and item. If the user lacks that permission, the source rejects the request with an authorization failure. ```prompt title="Choose a delegated-auth surface" In my Rayfin project, help me decide which delegated-auth surface to use for a Microsoft resource integration. Start from Fabric SSO as the identity source. If I need custom server-side code to call a Microsoft resource, use a function connection with udf.connection({ audienceType }) and ctx.getToken(AudienceType.X). If I need to query an existing Fabric source through typed entities or DAX/KQL, use a connector and set auth.type in rayfin/rayfin.yml. If the data is owned by my Rayfin app, use @role() or @authenticated() policies on rayfin/data entities. Explain which signed-in user or app identity each call runs as, what permissions the caller needs on the target Fabric item, and which Rayfin files I should edit. ``` --- --- title: "Fabric SSO" description: "Sign users in with their Microsoft Entra identity through the Fabric portal — enable it in rayfin.yml, install the provider package, and call ensureSignedInWithFabric." url: https://rayfin.ai/docs/auth/fabric-sso markdown_url: https://rayfin.ai/docs/auth/fabric-sso.md section: auth product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:25-07:00 source: auth/fabric-sso.mdx --- # Fabric SSO > Sign users in with their Microsoft Entra identity through the Fabric portal — enable it in rayfin.yml, install the provider package, and call ensureSignedInWithFabric. Fabric SSO lets users sign in to a deployed Rayfin app with the Microsoft Entra identity they already used to open the Fabric portal. There's no separate login form — the user authenticates once in Fabric and the app inherits that session. > [!NOTE] > Fabric SSO requires a *backend* deployed to Fabric — not a frontend deployed to Fabric. > The popup flow signs users in from any origin listed in `allowedRedirectUris`, including > a local Vite dev server at `http://localhost:5173`. Only the embedded flow needs the app > to be running inside the Fabric portal. See > [Local frontend development](#local-frontend-development). The SDK supports two flows: * **Popup flow** — the app opens the Fabric portal in a new browser tab, the user authenticates, and the tab closes automatically. * **Embedded flow** — the app runs inside a Fabric iframe and inherits the session via `postMessage`, with no popup, redirect, or user interaction. Both flows use PKCE (S256), validate the `postMessage` origin, and tie the exchange to a state nonce. ## How it works [#how-it-works] ### Popup flow [#popup-flow] 1. The app opens the Fabric portal in a new browser tab and registers a `postMessage` listener. 2. The user authenticates through Entra ID inside the Fabric portal. 3. The Fabric extension sends the handoff code back to the app via `window.top.opener.postMessage()`. 4. The SDK exchanges the handoff code for Rayfin session tokens and creates a session. 5. The Fabric tab closes automatically. No callback page or redirect is needed. ### Embedded flow (Fabric iframe) [#embedded-flow-fabric-iframe] 1. The Fabric shell loads the app inside an iframe with `?fabricEmbedded=true` in the URL. 2. On startup the app detects embedded mode and calls `initEmbeddedAuth()`. 3. The SDK generates PKCE parameters in memory and sends `auth.requestHandoff` to the parent frame via `postMessage`. 4. The Fabric extension host responds with a handoff code. 5. The SDK exchanges the handoff code for Rayfin session tokens and creates a session. No popup, redirect, or user click is needed. ## Local frontend development [#local-frontend-development] The popup flow works from `localhost`. What Fabric SSO needs is a deployed **backend** — the frontend calling it can be served by Vite: 1. Deploy the backend, skipping the static frontend: ```bash npx rayfin up --exclude-services staticHosting ``` 2. Confirm your dev origin is in `allowedRedirectUris` — `http://localhost:5173` is the default. 3. Start Vite and sign in from the button that calls `ensureSignedInWithFabric()`: ```bash npm run dev ``` The popup opens the Fabric portal, the user authenticates there, and the handoff code is posted back to `returnOrigin` — your `localhost` origin. Sessions, claims, and row-level security behave exactly as they do in the deployed app. The embedded flow is the exception. It needs the Fabric shell to load your app in an iframe, so it only runs once `npx rayfin up` has deployed the static frontend and you open the app from the Fabric portal. `ensureSignedInWithFabric()` falls back to the popup when the app isn't embedded, so the same call covers both cases. ## Enable Fabric SSO [#enable-fabric-sso] Add the `fabric` block and your app's origin to `rayfin/rayfin.yml`: ```yaml title="rayfin/rayfin.yml" services: auth: enabled: true allowedRedirectUris: - http://localhost:5173 fabric: enabled: true ``` `allowedRedirectUris` must include your app's bare origin (for example `http://localhost:5173`) — the popup flow uses it as the `postMessage` target origin for the handoff code. See [Redirect URIs](/docs/hosting/redirect-uris) for the full picture, including what `rayfin up` appends automatically on deploy. After changing `rayfin.yml`, redeploy so the setting takes effect: ```bash npx rayfin up ``` ## Install the provider package [#install-the-provider-package] Fabric auth ships as a separate companion package: ```bash npm install @microsoft/rayfin-auth-provider-fabric ``` ## Client-side usage [#client-side-usage] ### Popup flow: sign in from a button click [#popup-flow-sign-in-from-a-button-click] Call `ensureSignedInWithFabric` from a user-gesture handler, such as a button's `onClick`. The function's last step calls `window.open()`, so it needs a synchronous user gesture to avoid popup blockers. ```typescript title="src/services/rayfinClient.ts" import { RayfinClient } from '@microsoft/rayfin-client'; import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric'; const client = new RayfinClient({ baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, }); async function handleSignIn() { const session = await ensureSignedInWithFabric(client.auth, { workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID, projectId: import.meta.env.VITE_FABRIC_ITEM_ID, fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL, returnOrigin: window.location.origin, }); console.log('Signed in:', session.user); } ``` ### Embedded flow: automatic sign-in on startup [#embedded-flow-automatic-sign-in-on-startup] Call `initEmbeddedAuth()` once at app startup — for example in a React `useEffect` or an initialization routine. It's safe to call on every page load: it returns `null` immediately when the app isn't running in embedded mode. ```typescript title="src/services/bootstrap.ts" import { RayfinClient } from '@microsoft/rayfin-client'; import { initEmbeddedAuth } from '@microsoft/rayfin-auth-provider-fabric'; const client = new RayfinClient({ baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, }); const fabricOptions = { workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID, projectId: import.meta.env.VITE_FABRIC_ITEM_ID, fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL, returnOrigin: window.location.origin, }; // Safe to call on every page load — a no-op when not embedded. const session = await initEmbeddedAuth(client.auth, fabricOptions); if (session) { console.log('Embedded session established:', session.user); } ``` > [!WARNING] > Import `@microsoft/rayfin-auth-provider-fabric` statically in your app's entry module — > not only via a dynamic `import()`. The package captures the `?fabricEmbedded=true` URL flag > into `sessionStorage` as a side effect of module load, and that has to happen on the > initial page load, before any client-side navigation strips the query string (for example a > post-logout redirect to `/login`). An app that resumes from a stored refresh token never > takes the embedded-auth path on first load, so a dynamic-only import would miss the URL > flag and fall back to the popup on the next sign-in. ### Supporting both flows [#supporting-both-flows] Most apps should support both the popup flow (standalone browser) and the embedded flow (iframe). `ensureSignedInWithFabric()` handles this automatically — it tries embedded auth first, then falls back to the popup: 1. Return the existing session if already authenticated. 2. Attempt a silent refresh via the refresh token. 3. If embedded mode is detected, use the `postMessage` handoff (no popup). 4. Otherwise, open the Fabric portal in a new tab and wait for the handoff. For page-load initialization with no user gesture available, use `initEmbeddedAuth()` instead — it skips step 4 and returns `null` when no embedded session is available. ### React hook example [#react-hook-example] ```typescript title="src/hooks/useFabricAuth.ts" import { useState, useCallback } from 'react'; import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric'; import { client } from '../services/rayfinClient'; const fabricOptions = { workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID, projectId: import.meta.env.VITE_FABRIC_ITEM_ID, fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL, returnOrigin: window.location.origin, }; export function useFabricAuth() { const [session, setSession] = useState(client.auth.getSession()); const signIn = useCallback(async () => { const result = await ensureSignedInWithFabric(client.auth, fabricOptions); setSession(result); return result; }, []); return { session, signIn, isAuthenticated: session?.isAuthenticated ?? false }; } ``` See [React integration](/docs/auth/react) for a fuller pattern that also covers local development and route guarding. ## API reference [#api-reference] ### `ensureSignedInWithFabric(auth, options)` [#ensuresignedinwithfabricauth-options] ```typescript function ensureSignedInWithFabric( auth: Auth, options: FabricAuthOptions ): Promise; ``` The primary entry point. Implements the four-step waterfall described above — the first step that succeeds short-circuits the rest. Steps 1–3 are safe to call on page load; step 4 opens a new browser tab and must run inside a user-gesture handler. ### `initEmbeddedAuth(auth, options)` [#initembeddedauthauth-options] ```typescript function initEmbeddedAuth( auth: Auth, options: FabricAuthOptions ): Promise; ``` Call once at app startup. Returns the authenticated session when running in embedded mode, or `null` when not embedded. Never opens a popup or new tab — safe for page-load use. ### `initiateFabricLogin(auth, options)` [#initiatefabricloginauth-options] ```typescript function initiateFabricLogin(auth: Auth, options: FabricAuthOptions): Promise; ``` Low-level function that opens the Fabric portal and listens for the `postMessage` handoff, with no session or refresh-token pre-checks. Called internally by `ensureSignedInWithFabric` in step 4. Most apps should use `ensureSignedInWithFabric` instead. ### `isEmbeddedMode(options)` [#isembeddedmodeoptions] ```typescript function isEmbeddedMode(options: FabricAuthOptions): boolean; ``` Reports whether the SDK considers the current page to be running in embedded mode, without starting a sign-in flow. Detection is `true` when `options.fabricEmbedded` is `true`, the URL contains `?fabricEmbedded=true`, or a previous call already stored that flag in `sessionStorage`. ### `FabricAuthOptions` [#fabricauthoptions] | Property | Type | Description | | ----------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `workspaceId` | `string` | The Fabric workspace ID. | | `projectId` | `string` | The Rayfin item ID (the AppBackend artifact ID). | | `fabricPortalUrl` | `string` | The Fabric portal base URL, e.g. `https://app.fabric.microsoft.com`. Existing path and query parameters are preserved. | | `returnOrigin` | `string` | Your app's bare origin, e.g. `window.location.origin`. Used as the `postMessage` target origin. | | `fabricEmbedded` | `boolean` (optional) | Force embedded mode. The SDK also auto-detects it from `?fabricEmbedded=true` in the URL. | | `callbackUrl` | `string` (optional, deprecated) | Full callback URL for legacy Fabric portals that redirect instead of using `postMessage`. Defaults to `${returnOrigin}/auth/callback`. Only needed for backward compatibility — will be removed once the `postMessage` rollout is complete. | ### Legacy callback bridge [#legacy-callback-bridge] Newer Fabric portals hand off the sign-in code via `postMessage`. Older portals instead redirect the popup to a callback page in your app. For that legacy path, call `bridgeFabricCallback()` as early as possible on the callback page — if the URL contains Fabric handoff parameters, it forwards them to the opener window and closes the popup. ```typescript title="src/pages/AuthCallback.tsx" import { bridgeFabricCallback } from '@microsoft/rayfin-auth-provider-fabric'; const bridged = bridgeFabricCallback(); if (!bridged) { console.log('No Fabric handoff detected'); } ``` This function is deprecated — it exists only for backward compatibility with Fabric portals that haven't adopted `postMessage` yet, and will be removed once that rollout completes. ## Security [#security] * **PKCE S256** — every flow generates a cryptographic code verifier and challenge to prevent authorization code interception. * **State nonce** — a random nonce ties the `postMessage` response to the originating flow, preventing CSRF. * **In-closure code verifier** — the PKCE code verifier is held in memory only and never persisted to `localStorage`. * **Origin validation** — the SDK validates `event.origin` on incoming messages against `fabricPortalUrl`; the Fabric extension uses an explicit `targetOrigin` (never `"*"`) when sending the handoff code. * **Flow timeout** — the flow times out after 5 minutes if no `postMessage` is received. * **Session isolation** — in embedded mode, the session lives in the iframe's own `localStorage`, isolated from the parent frame by the browser's same-origin policy. ## Environment variables [#environment-variables] Fabric auth needs three values at runtime to build `FabricAuthOptions`. `npx rayfin up` writes the underlying `RAYFIN_PUBLIC_*` values to `rayfin/.env`, and `rayfin env --framework vite` (run automatically by the scaffolded `predev` / `prebuild` hooks) maps them to Vite-compatible names in `.env.local`. | Source variable (`rayfin/.env`) | Vite variable (`.env.local`) | Maps to | Example | | ------------------------------- | ---------------------------- | ----------------- | -------------------------------------- | | `RAYFIN_PUBLIC_ITEM_ID` | `VITE_FABRIC_ITEM_ID` | `projectId` | `21b98705-08d5-448c-ab32-d88a3d00af41` | | `RAYFIN_PUBLIC_WORKSPACE_ID` | `VITE_FABRIC_WORKSPACE_ID` | `workspaceId` | `b80c0e39-468a-4742-8f0a-458dc6b1c918` | | `RAYFIN_PUBLIC_PORTAL_URL` | `VITE_FABRIC_PORTAL_URL` | `fabricPortalUrl` | `https://app.fabric.microsoft.com/` | For local development, add these to `rayfin/.env` directly: ```text title="rayfin/.env" RAYFIN_PUBLIC_ITEM_ID= RAYFIN_PUBLIC_WORKSPACE_ID= RAYFIN_PUBLIC_PORTAL_URL=https://app.fabric.microsoft.com/ ``` ## Deployment values [#deployment-values] After `npx rayfin up`, the CLI records deployment metadata in `rayfin/.deployments.json` and merges the corresponding `RAYFIN_PUBLIC_*` variables into `rayfin/.env`. Run `rayfin env --framework vite` (or `npm run dev`, which triggers it via the scaffolded `predev` hook) to regenerate `.env.local` with the Vite-compatible names — use `VITE_FABRIC_ITEM_ID` as `projectId` and `VITE_FABRIC_WORKSPACE_ID` as `workspaceId` in your `FabricAuthOptions`. ## Troubleshooting [#troubleshooting] * **Popup blocked** — call `ensureSignedInWithFabric` from a synchronous user-gesture handler, such as a button's `onClick`. Calling it on page load, or after an `await` and before the user clicks, triggers popup blockers. * **Session not persisting** — confirm `RayfinClient` is configured with the correct `baseUrl` and `publishableKey`. * **Timeout after 5 minutes** — the handoff code was never received. Check that `returnOrigin` matches your app's actual origin and that the Fabric extension is sending to the correct origin. * **Origin mismatch** — `fabricPortalUrl` must match the origin of the Fabric portal tab. Verify you're using the correct URL for your environment (production, PPE, or dev). * **`initEmbeddedAuth` returns `null`** — ensure the URL contains `?fabricEmbedded=true`, or set `fabricEmbedded: true` explicitly in the options. * **Embedded handoff timeout** — the parent frame never responded. Verify `returnOrigin` matches the iframe's actual origin. * **State mismatch error** — the response state didn't match the request state. This can indicate a replay attack or a stale response from a previous flow. ```prompt title="Add Fabric SSO sign-in" Add Fabric SSO sign-in to my Rayfin app. Enable services.auth.fabric in rayfin/rayfin.yml (keep allowedRedirectUris scoped to my app's own origin), install @microsoft/rayfin-auth-provider-fabric, and wire up ensureSignedInWithFabric() from a button click handler for the popup flow, plus initEmbeddedAuth() in a startup effect for the embedded/iframe flow. Import @microsoft/rayfin-auth-provider-fabric statically at the app's entry point, not via a dynamic import — it needs to capture the ?fabricEmbedded=true URL flag on first load. Use onSessionChange to react to session changes — onAuthStateChange does not exist. Gate UI on isAuthenticated or the presence of a user property rather than introspecting the session. Make sure my dev origin is in services.auth.allowedRedirectUris so I can test the popup flow from my local Vite server — it only needs the backend deployed to Fabric, not the frontend. Tell me the exact commands to deploy the backend and start the dev server. ``` --- --- title: "Auth" description: "Sign users in with Fabric SSO — Rayfin's managed sessions, per-user data isolation, and one client API for every deployed app." url: https://rayfin.ai/docs/auth markdown_url: https://rayfin.ai/docs/auth.md section: auth product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: auth/index.mdx --- # Auth > Sign users in with Fabric SSO — Rayfin's managed sessions, per-user data isolation, and one client API for every deployed app. Rayfin Auth gives you managed sign-in and session handling without building an identity service yourself. Enable it in `rayfin.yml`, call `ensureSignedInWithFabric(client.auth, ...)`, and every `client.data.*` call after that automatically carries the authenticated context — no manual header or token passing between modules. ## Why use Rayfin Auth [#why-use-rayfin-auth] * **Zero auth infrastructure** — sessions and token handling work with no external identity service to deploy or configure. * **Pre-integrated with data** — after sign-in, every `client.data.*` call automatically carries the authenticated context. * **Automatic per-user data isolation** — JWT claims drive the row-level security policies you declare on your entities (see [Permissions](/docs/data/permissions)), so each user sees only the data they own without hand-written SQL. * **One client API** — `getSession`, `onSessionChange`, and `signOut` work the same way no matter which Fabric SSO flow (popup or embedded) signed the user in. ## Fabric SSO is the only auth method [#fabric-sso-is-the-only-auth-method] Rayfin apps sign users in with the Microsoft Entra identity they already used to open the Fabric portal. There's no separate login form, sign-up step, or password to manage — the user authenticates once in Fabric and the app inherits that session. > [!NOTE] > Sign-in works from a local dev server as well as from the deployed app. The popup flow > returns its handoff code to any origin listed in `allowedRedirectUris`, including > `http://localhost:5173` — what it requires is a *backend* deployed to Fabric. Only the > embedded (iframe) flow needs the app itself to be running inside the Fabric portal. See > [Fabric SSO](/docs/auth/fabric-sso) for both flows. ## Configuring auth [#configuring-auth] Enable the service and Fabric SSO in `rayfin/rayfin.yml`: ```yaml title="rayfin/rayfin.yml" services: auth: enabled: true allowedRedirectUris: - http://localhost:5173 fabric: enabled: true ``` Redeploy (`npx rayfin up`) after changing this file — auth settings are synced to the remote service on every deploy. See [Redirect URIs](/docs/hosting/redirect-uris) for what `allowedRedirectUris` controls. ## In this section [#in-this-section] * **[Fabric SSO](/docs/auth/fabric-sso)** — enable it, install the provider package, and call `ensureSignedInWithFabric`. * **[Sessions](/docs/auth/sessions)** — the opaque session object, `onSessionChange`, and token handling. * **[Delegated access](/docs/auth/delegated-access)** — how functions, connectors, and entity policies use the signed-in user's identity. * **[React integration](/docs/auth/react)** — an auth context, a hook, and route guarding. ```prompt title="Wire up Rayfin auth with Fabric SSO" In my Rayfin app, set up authentication: - In rayfin/rayfin.yml, enable services.auth with fabric.enabled: true, and keep allowedRedirectUris scoped to my app's own origin(s), including http://localhost:5173 for local frontend development. - Install @microsoft/rayfin-auth-provider-fabric and call ensureSignedInWithFabric from a button click handler for the popup flow, plus initEmbeddedAuth in a startup effect for the embedded/iframe flow. - Use onSessionChange to react to session changes — onAuthStateChange does not exist on the Rayfin auth client. Gate UI on isAuthenticated or the presence of a user object, never on session internals. Explain which files you changed and how to test sign-in. ``` --- --- title: "React integration" description: "An auth context, a useAuth hook, and route guarding for React apps built on the Rayfin auth client with Fabric SSO." url: https://rayfin.ai/docs/auth/react markdown_url: https://rayfin.ai/docs/auth/react.md section: auth product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:25-07:00 source: auth/react.mdx --- # React integration > An auth context, a useAuth hook, and route guarding for React apps built on the Rayfin auth client with Fabric SSO. Rayfin's auth client is framework-agnostic — `getSession` and `onSessionChange` work the same everywhere, and Fabric SSO's `ensureSignedInWithFabric` resolves to the same opaque session shape. This page wires them into React with a context, a hook, and route guarding. ## A minimal `useAuth` hook [#a-minimal-useauth-hook] The smallest useful integration reads the current session once on mount, subscribes to changes, and exposes a `signIn` that wraps `ensureSignedInWithFabric`: ```typescript title="src/hooks/useAuth.ts" import { useState, useEffect, useCallback } from 'react'; import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric'; import { auth } from '../services/rayfinClient'; import type { OpaqueSession } from '@microsoft/rayfin-auth'; const fabricOptions = { workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID, projectId: import.meta.env.VITE_FABRIC_ITEM_ID, fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL, returnOrigin: window.location.origin, }; export function useAuth() { const [session, setSession] = useState(null); useEffect(() => { setSession(auth.getSession()); return auth.onSessionChange(setSession); }, []); const signIn = useCallback(() => ensureSignedInWithFabric(auth, fabricOptions), []); return { ...session, isAuthenticated: session?.isAuthenticated ?? false, signIn, signOut: auth.signOut.bind(auth), }; } ``` This is enough for a single-page app. Most real apps need one more thing: a shared provider so every component sees the same session without each one re-subscribing to `onSessionChange` itself. The rest of this page builds that. ## An auth context and provider [#an-auth-context-and-provider] `src/services/rayfinClient.ts` creates the shared `RayfinClient` and the `FabricAuthOptions` the provider needs: ```typescript title="src/services/rayfinClient.ts" import { RayfinClient } from '@microsoft/rayfin-client'; export const client = new RayfinClient({ baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, }); export const auth = client.auth; export const fabricOptions = { workspaceId: import.meta.env.VITE_FABRIC_WORKSPACE_ID, projectId: import.meta.env.VITE_FABRIC_ITEM_ID, fabricPortalUrl: import.meta.env.VITE_FABRIC_PORTAL_URL, returnOrigin: window.location.origin, }; ``` The provider owns the session state, exposes loading/error state during sign-in, and hands the same `signIn` / `signOut` functions to every consumer: ```tsx title="src/hooks/AuthContext.tsx" import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from 'react'; import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric'; import type { OpaqueSession } from '@microsoft/rayfin-auth'; import { auth, fabricOptions } from '../services/rayfinClient'; interface AuthContextValue { session: OpaqueSession | null; loading: boolean; error: string | null; signIn: () => Promise; signOut: () => Promise; isAuthenticated: boolean; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { setSession(auth.getSession()); setLoading(false); return auth.onSessionChange(setSession); }, []); const signIn = useCallback(async () => { setError(null); setLoading(true); try { const result = await ensureSignedInWithFabric(auth, fabricOptions); setSession(result); return result; } catch (err) { setError(err instanceof Error ? err.message : 'Sign-in failed'); throw err; } finally { setLoading(false); } }, []); const signOut = useCallback(async () => { await auth.signOut(); setSession(null); setError(null); }, []); const value = useMemo( () => ({ session, loading, error, signIn, signOut, isAuthenticated: session?.isAuthenticated ?? false, }), [session, loading, error, signIn, signOut] ); return {children}; } export function useAuth(): AuthContextValue { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; } ``` The provider talks to `client.auth` directly and owns the session state — every component that calls `useAuth()` sees the same session without re-subscribing to `onSessionChange` itself. ## Guarding routes [#guarding-routes] A guard component reads `useAuth()` and redirects based on `isAuthenticated`, showing a loading state until the first session check resolves: ```tsx title="src/App.tsx" import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { AuthPage } from './components/AuthPage'; import { useAuth } from './hooks/AuthContext'; import { HomePage } from './pages/HomePage'; function AuthGuard({ children, requireAuth, }: { children: React.ReactNode; requireAuth: boolean; }) { const { isAuthenticated, loading } = useAuth(); if (loading) return
Loading…
; if (requireAuth && !isAuthenticated) return ; if (!requireAuth && isAuthenticated) return ; return <>{children}; } function App() { return ( } /> } /> } /> ); } export default App; ``` ## Sign-in button and app entry point [#sign-in-button-and-app-entry-point] `ensureSignedInWithFabric`'s popup fallback calls `window.open()`, so `signIn` has to run from a synchronous user-gesture handler — wire it to a button, not an effect: ```tsx title="src/components/AuthPage.tsx" import { useAuth } from '../hooks/AuthContext'; export function AuthPage() { const { signIn, loading, error } = useAuth(); return (
{error &&

{error}

}
); } ``` Wrap the app in `AuthProvider` at the entry point: ```tsx title="src/main.tsx" import { createRoot } from 'react-dom/client'; import App from './App'; import { AuthProvider } from './hooks/AuthContext'; createRoot(document.getElementById('root')!).render( ); ``` > [!NOTE] > Sign-in completes from a local Vite dev server as long as your origin is in > `allowedRedirectUris` and the backend is deployed — see > [Local frontend development](/docs/auth/fabric-sso#local-frontend-development). Everything > else in this pattern — the provider, the guard, `isAuthenticated` — behaves the same > regardless of which flow produced the session. ```prompt title="Add an AuthProvider, useAuth hook, and route guarding" In my Rayfin React app (Vite + react-router-dom), add authentication wiring: - An AuthProvider (React context) that wraps the app, calls client.auth.getSession() once on mount, subscribes with client.auth.onSessionChange(...), and exposes { session, loading, error, signIn, signOut, isAuthenticated } through a useAuth() hook. - signIn() should call ensureSignedInWithFabric() from @microsoft/rayfin-auth-provider-fabric, built from VITE_FABRIC_WORKSPACE_ID, VITE_FABRIC_ITEM_ID, and VITE_FABRIC_PORTAL_URL, and must be wired to a button's onClick so the popup isn't blocked. - An AuthGuard component used in the router that redirects unauthenticated users to /auth and authenticated users away from /auth, showing a loading state in between. Use onSessionChange, not onAuthStateChange — the latter does not exist on the Rayfin auth client. Do not introspect session fields beyond isAuthenticated, isAnonymous, user, role, and expiresAt. Show me the full set of files. ``` --- --- title: "Sessions" description: "How Rayfin's opaque session object works — subscribe with onSessionChange, gate UI on isAuthenticated, and let Rayfin manage tokens for you." url: https://rayfin.ai/docs/auth/sessions markdown_url: https://rayfin.ai/docs/auth/sessions.md section: auth product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: auth/sessions.mdx --- # Sessions > How Rayfin's opaque session object works — subscribe with onSessionChange, gate UI on isAuthenticated, and let Rayfin manage tokens for you. Every Rayfin session comes from signing in with Fabric SSO — the popup and embedded flows both produce the same opaque session object. Once you know how to read it, it doesn't matter which flow completed it. ## Sessions are opaque [#sessions-are-opaque] `client.auth.getSession()` returns an `OpaqueSession`. Treat it as opaque: read the fields below, but don't infer meaning from anything else it might contain internally. ```typescript interface OpaqueSession { user: User | null; role?: string; expiresAt?: Date; isAuthenticated: boolean; isAnonymous: boolean; } ``` | Field | Description | | ----------------- | ------------------------------------------------- | | `user` | The signed-in user, or `null` if there isn't one. | | `isAuthenticated` | `true` once a user has a valid session. | | `isAnonymous` | `true` for an anonymous/unauthenticated session. | | `role` | The user's role, if your project assigns one. | | `expiresAt` | When the current access token expires. | > [!WARNING] > Gate UI logic on `isAuthenticated` or the presence of `user` — not on internal session > fields. Rayfin's session shape is intentionally opaque so it can change without breaking > your app, as long as you only depend on the documented fields above. ```typescript const session = client.auth.getSession(); if (session.isAuthenticated && session.user) { showApp(session.user); } else { showSignInPage(); } ``` ## Subscribing to session changes: `onSessionChange` [#subscribing-to-session-changes-onsessionchange] > [!WARNING] > The method is `onSessionChange`. **`onAuthStateChange` does not exist** on the Rayfin auth > client — code that calls it will throw at runtime, not just fail a type check. `onSessionChange` fires whenever the session changes — sign-in, sign-out, token refresh, user update, or expiration — and returns an unsubscribe function: ```typescript const unsubscribe = client.auth.onSessionChange((session) => { if (session?.isAuthenticated) { console.log('Signed in as', session.user?.email); } else { console.log('Signed out'); } }); // Later, e.g. on component unmount: unsubscribe(); ``` See [React integration](/docs/auth/react) for wiring this into a `useAuth()` hook. ## Reading the current session once [#reading-the-current-session-once] `getSession()` returns synchronously and reflects whatever Rayfin currently has in memory — call it once on startup, then rely on `onSessionChange` for updates: ```typescript const session = client.auth.getSession(); ``` ## Token handling [#token-handling] In normal use, Rayfin manages access tokens internally and attaches them to `client.data.*` and `client.functions.*` calls automatically — your code never reads, stores, or forwards a token itself. * `hasRefreshToken()` reports whether the session has a refresh token available. * `refreshSession()` refreshes the session using the stored refresh token and resolves with the new token response; it rejects with an `AuthError` if there's no refresh token or the refresh fails. This exists to satisfy the refresh contract, not as a supported way to extract a token for reuse elsewhere — it still requires an existing browser session with a stored refresh token, which is exactly what a Node.js script never has. See [Seeding data](/docs/data/seeding#what-a-seed-script-can-authenticate-as) for what a script can do without one. * Rayfin schedules a refresh automatically as the access token approaches expiry when a refresh token is available, so you generally don't need to call `refreshSession()` yourself. ```typescript if (client.auth.hasRefreshToken()) { try { await client.auth.refreshSession(); } catch { // Refresh failed — the user needs to sign in again. } } ``` ## Signing out [#signing-out] ```typescript await client.auth.signOut(); // End the current session. await client.auth.signOutAll(); // Revoke every session for this user across every // device, returning the number revoked. ``` ## Auth events [#auth-events] `onSessionChange` covers most UI needs. For finer-grained handling, subscribe to individual events with `on(event, handler)`: | Event | Fires when | | ---------------------- | --------------------------------------------------------- | | `AUTH_LOGIN` | A session is established via Fabric SSO. | | `AUTH_LOGOUT` | The user signs out. | | `AUTH_REFRESH` | The session is refreshed. | | `AUTH_SESSION_EXPIRED` | The access token expires with no refresh token available. | ```typescript const unsubscribe = client.auth.on('AUTH_SESSION_EXPIRED', () => { redirectToSignIn(); }); ``` ```prompt title="Gate a page behind authentication" In my Rayfin React app, gate the main app route behind authentication: - On mount, read client.auth.getSession() once, then subscribe with client.auth.onSessionChange(...) to keep it in sync. Do not use onAuthStateChange — it does not exist on the Rayfin auth client. - Show a loading state until the first session check completes, then render the app if session.isAuthenticated and session.user are both present, or redirect to a sign-in page otherwise. - Do not introspect any session fields beyond isAuthenticated, isAnonymous, user, role, and expiresAt — treat the session object as opaque. Show me the resulting component. ``` --- --- title: "Adding a connector" description: "Discover Fabric sources, add a connector to rayfin.yml, install its pinned packages, and manage connector entries safely." url: https://rayfin.ai/docs/connectors/adding markdown_url: https://rayfin.ai/docs/connectors/adding.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/adding.mdx --- # Adding a connector > Discover Fabric sources, add a connector to rayfin.yml, install its pinned packages, and manage connector entries safely. Add a connector when a Rayfin app needs to reach existing Fabric data. Start with `connector search` if you do not already have the workspace ID, item ID, and connector type. See [Connectors](/docs/connectors) for the connector-type catalog. > [!WARNING] > Connectors are in private preview. The `rayfin connector` command group is hidden until > the project opts in, and the APIs may change between releases. ## Find a source [#find-a-source] `connector search` lists Fabric items the signed-in identity can add and prints the exact `connector add` command for each result. ```bash npx rayfin connector search [query] [--workspace-id --type ] [--limit ] [--json] ``` Choose one scope: | Scope | Command shape | Notes | | --------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | One workspace | `npx rayfin connector search "sales" --workspace-id --type fabric-warehouse` | `--type` is required so the CLI makes a bounded request. | | Every accessible workspace | `npx rayfin connector search --all-workspaces --type fabric-warehouse,fabric-sqldatabase` | `--type` is required because it is the server-side filter for a tenant-wide scan. | | Deployed project workspaces | `npx rayfin connector search` | Run inside a deployed Rayfin project with no scope flag. The CLI searches the workspaces recorded in the deployments registry. | Use `--limit ` to cap plain or JSON output. Interactive output ignores the limit because the picker paginates the full result set. Use `--json` when an agent or script will choose the result. The JSON envelope includes `status`, `query`, `scope`, `count`, and `sources`. Each source includes `workspaceId`, `itemId`, `connectorType`, `suggestedName`, and a ready-to-run `addCommand`. ```json { "status": "ok", "query": "sales", "scope": { "workspaceId": "" }, "count": 1, "limit": 5, "sources": [ { "workspaceId": "", "workspaceName": "Finance", "itemId": "", "itemType": "Warehouse", "displayName": "Inventory", "connectorType": "fabric-warehouse", "suggestedName": "inventory", "addCommand": "rayfin connector add --type fabric-warehouse --workspace-id --item-id --name inventory" } ] } ``` ## Add the connector [#add-the-connector] Run `connector add` with literal Fabric IDs. The flag is `--type `, not `--connector`. ```bash npx rayfin connector add --type --workspace-id --item-id ``` | Flag | Required | Purpose | | --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `--type ` | Yes | Connector type, such as `fabric-warehouse`, `fabric-sqldatabase`, `fabric-sqlanalytics`, `fabric-semanticmodel`, or `kusto`. | | `--workspace-id ` | Yes | Fabric workspace ID. It must be a literal value; `${VAR}` placeholders are rejected. | | `--item-id ` | Yes | Fabric item ID. It must be a literal value. | | `--name ` | No | Connector name. When omitted, the CLI derives it from the Fabric item display name. | | `--operations ` | No | Comma-separated subset of the catalog operations, for example `read,update`. Omit it to use every allowed operation. | | `-y`, `--yes` | No | Accept overwrite and confirmation prompts. | | `-v`, `--verbose` | No | Print verbose diagnostics. | | `--json` | No | Emit machine-readable output. | Connector names must match `/^[a-zA-Z0-9\-_]+$/` and can be at most 256 characters. ## Know what the command writes [#know-what-the-command-writes] `connector add` verifies the Fabric item, writes a `connectors:` entry in `rayfin/rayfin.yml`, and scaffolds `rayfin/connectors//schema.ts`. For Category A connectors, it also runs schema discovery and writes `rayfin/connectors//metadata.json`. For `kusto`, it resolves the KQL Database's cluster query endpoint and database name from `(workspaceId, itemId)`, then bakes `queryServiceUri` and `databaseName` into the generated `schema.ts`. The `rayfin.yml` entry uses an array. Operations are objects, not bare strings: ```yaml title="rayfin/rayfin.yml" connectors: - name: inventory type: fabric-warehouse config: workspaceId: itemId: auth: type: delegated operations: - name: read - name: update - name: telemetry type: kusto version: '1' config: workspaceId: itemId: auth: type: delegated operations: - name: executeQuery - name: executeCommand ``` ## Scope operations [#scope-operations] Use `--operations` to narrow a connector below the catalog default: ```bash npx rayfin connector add --type fabric-warehouse --workspace-id --item-id --operations read,update ``` Rules: * You can narrow below the catalog default, but you cannot widen above it. * There is no `all` meta-operation. List every action explicitly, or omit `--operations` to accept the catalog default. * The host validator rejects unknown or duplicate operation names at `rayfin up` time. For the current connector types, the default operations are listed in [Connectors](/docs/connectors). ## Install the pinned packages [#install-the-pinned-packages] `connector add` scaffolds files but installs nothing. On success, it prints an exact version-pinned install command: ```text 📦 Install the packages this connector needs: npm install @microsoft/rayfin-connector-kusto@1.35.0-alpha ``` Run the command verbatim. Do not drop the version. Connector packages ship in lockstep with the CLI, but their npm `latest` and `preview` tags lag. An unversioned install can pull an older connector that hard-pins its own `@microsoft/rayfin-data`, leaving two Rayfin version lines in one app. `rayfin connector types --json` carries the same `packages` array, so an agent can rebuild the exact install list from the live catalog when needed. ## List and remove connectors [#list-and-remove-connectors] `connector list` reads the local `rayfin.yml` file and does not make network calls. ```bash npx rayfin connector list npx rayfin connector list -v npx rayfin connector list --json ``` Use `connector remove` with `-y` or `--yes` to remove both the `rayfin.yml` entry and the connector directory: ```bash npx rayfin connector remove --yes ``` Re-add a connector after removing it when you need to refresh `metadata.json` from the source. ## Next step [#next-step] After the connector entry and packages are in place, wire it into your app with [Wiring connectors into your app](/docs/connectors/client-setup). For the complete CLI flag reference, see [`rayfin connector`](/docs/reference/cli/connector). ```prompt title="Add a connector to a Rayfin project" In my Rayfin project, add a connector to an existing Microsoft Fabric source. If I have not provided a connector type, workspace ID, and item ID, run `npx rayfin connector search` with the right scope to find candidates, and use the `addCommand` from `--json` output rather than inventing IDs. Run the add command with `--type `, `--workspace-id `, and `--item-id ` using literal IDs. Use `--name ` only if I provided a name or the derived name is unsuitable. Use `--operations ` only to narrow below the catalog default. After the add command succeeds, run the exact version-pinned `npm install` command it prints; do not drop package versions. Then list the configured connectors with `npx rayfin connector list` and prepare to wire the connector through `ConnectorsRayfinClient` from `@microsoft/rayfin-client/experimental`. ``` --- --- title: "Connector authentication" description: "Configure connector auth.type values, validation rules, and caller identity for delegated and application connector access." url: https://rayfin.ai/docs/connectors/auth markdown_url: https://rayfin.ai/docs/connectors/auth.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/auth.mdx --- # Connector authentication > Configure connector auth.type values, validation rules, and caller identity for delegated and application connector access. Choose connector authentication in `rayfin/rayfin.yml` before you deploy a connector. The auth block controls whether runtime queries use the signed-in user's identity or the app's identity. For the conceptual overview across auth, functions, connectors, and data policies, see [Delegated access](/docs/auth/delegated-access). For the sibling function surface, see [Function connections](/docs/functions/connections). > [!WARNING] > Connectors are in private preview. Confirm the feature is available in your tenant before > you design an app around it. ## Declare `auth.type` [#declare-authtype] Each connector entry has an `auth:` block: ```yaml auth: type: delegated ``` In context, the entry looks like this: ```yaml title="rayfin/rayfin.yml" connectors: - name: warehouse type: fabric-warehouse config: workspaceId: itemId: auth: type: delegated operations: - name: read - name: create - name: update - name: delete ``` `npx rayfin connector add ...` writes `auth.type: delegated` by default. The value is lowercase: `delegated` or `application`. Other values, including uppercase variants, fail validation. ## Use an allowed value for the connector type [#use-an-allowed-value-for-the-connector-type] `rayfin up` validates that `auth.type` is known and allowed for the connector type before deployment starts. | Type | Allowed auth | | ---------------------- | -------------------------- | | `fabric-sqlanalytics` | `delegated`, `application` | | `fabric-warehouse` | `delegated`, `application` | | `fabric-sqldatabase` | `delegated`, `application` | | `fabric-semanticmodel` | `delegated` only | | `kusto` | `delegated` only | Category B connectors are delegated-only. `auth.type: application` on `fabric-semanticmodel` or `kusto` is rejected by `rayfin up` before deployment. ## Run queries as the signed-in user [#run-queries-as-the-signed-in-user] Use `auth.type: delegated` when each connector query should run as the signed-in user. The runtime uses on-behalf-of token exchange, so the user needs their own permission on the Fabric workspace and item. Rayfin does not grant source access on the user's behalf. Delegated auth is what makes row-level policies meaningful for Category A connector entities. The `claims` object in an `@role()` policy represents the real caller, so a policy can compare `claims.sub` or `claims.email` to fields on the connector entity. See the [policy DSL](/docs/data/permissions) and [connector entity generation](/docs/connectors/entity-generation). ## Run queries as the app [#run-queries-as-the-app] Use `auth.type: application` only when a Category A connector should run as the app identity instead of the caller. It is available for `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase`. Because the caller's identity is no longer in play, row-level policies keyed on `claims` no longer distinguish users. Do not choose application auth for per-user access control. ## Separate discovery permissions from runtime auth [#separate-discovery-permissions-from-runtime-auth] Schema discovery during `npx rayfin connector add ...` runs under the developer's identity. For Fabric SQL sources, that developer needs SQL endpoint permissions on the source so the CLI can inspect tables and columns. Runtime auth is separate. After deployment, connector queries use the configured `auth.type`: the signed-in user for `delegated`, or the app identity for `application`. ```prompt title="Configure connector authentication" In my Rayfin project, review the connectors block in rayfin/rayfin.yml and set the correct auth.type for each connector. Use auth.type: delegated when queries should run as the signed-in user and the user has permission on the target Fabric workspace and item. Use auth.type: application only for fabric-sqlanalytics, fabric-warehouse, or fabric-sqldatabase when the connector should run as the app identity. Do not set application auth on fabric-semanticmodel or kusto because rayfin up rejects that combination. If the connector is Category A and uses @role() policies on generated entities, explain how the chosen auth type affects claims-based row-level security. Then run npx rayfin up far enough to catch connector validation errors and report the result. ``` --- --- title: "Wiring connectors into your app" description: "Configure ConnectorsRayfinClient with connector schemas, runtime hooks, and browser-safe schema imports." url: https://rayfin.ai/docs/connectors/client-setup markdown_url: https://rayfin.ai/docs/connectors/client-setup.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/client-setup.mdx --- # Wiring connectors into your app > Configure ConnectorsRayfinClient with connector schemas, runtime hooks, and browser-safe schema imports. Wire the generated connector files into the frontend after `rayfin connector add` has created the connector entry and you have installed the exact pinned packages it printed. Category A connectors can run with configuration only; Category B connectors also need runtime hooks. ## Import the experimental client [#import-the-experimental-client] `ConnectorsRayfinClient` is exported from the experimental subpath: ```typescript import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental'; ``` Do not import it from the stable `@microsoft/rayfin-client` entry. The type parameters are ``. Use `Record` for any slot your app does not use. ## Pass connector config in the first argument [#pass-connector-config-in-the-first-argument] The constructor is: ```typescript new ConnectorsRayfinClient(config, connectorsRuntime?) ``` `config` extends the standard Rayfin client config, so it accepts `baseUrl` and `publishableKey`. It also requires `connectors`, a map keyed by connector name whose values are the generated `connectorConfig` objects. `host` is optional. When omitted, the connectors layer detects `{ type: 'cli' }` under Node and `{ type: 'standalone' }` in browsers. Pass `{ type: 'embedded' }` only when the app is running inside the Fabric portal host and the host integration has asserted that environment. ## Pass Category B runtimes in the second argument [#pass-category-b-runtimes-in-the-second-argument] The second constructor argument is the per-connector runtime map, keyed by connector name. Category A connectors need no runtime entry. Category B connectors require one: * `kusto()` merges the generated `queryServiceUri` and `databaseName` into the outbound payload. Without it, KQL queries cannot route to the cluster. * `fabricSemanticModel()` decodes the Arrow response. Without it, semantic-model query results cannot be read. ## Keep connector keys identical [#keep-connector-keys-identical] The connector key must be identical in four places: 1. The `name` in `rayfin.yml`. 2. The property in `AppConnectorsSchema`. 3. The property in the `connectors` option. 4. The property in the runtime map for Category B connectors. If they differ, TypeScript reports `Property '' does not exist on connectors`. ## Wire one client [#wire-one-client] This example wires one Category A connector and both Category B connectors. It assumes `inventory`, `salesModel`, and `telemetry` are the exact connector names in `rayfin.yml`. ```typescript title="src/services/rayfinClient.ts" import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental'; import { fabricSemanticModel } from '@microsoft/rayfin-connector-fabric-semanticmodel'; import { kusto } from '@microsoft/rayfin-connector-kusto'; import { type InventorySchema, connectorConfig as inventoryConfig, } from '../../rayfin/connectors/inventory/schema'; import { type SalesModelSchema, connectorConfig as salesModelConfig, } from '../../rayfin/connectors/salesModel/schema'; import { type TelemetrySchema, connectorConfig as telemetryConfig, } from '../../rayfin/connectors/telemetry/schema'; type AppConnectorsSchema = { inventory: InventorySchema; salesModel: SalesModelSchema; telemetry: TelemetrySchema; }; export const rayfinClient = new ConnectorsRayfinClient< Record, Record, AppConnectorsSchema >( { baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, authStorage: true, connectors: { inventory: inventoryConfig, salesModel: salesModelConfig, telemetry: telemetryConfig, }, }, { salesModel: fabricSemanticModel(), telemetry: kusto(), }, ); ``` ## Keep entity classes out of browser bundles [#keep-entity-classes-out-of-browser-bundles] > [!WARNING] > `schema.ts` is imported by browser code because the client reads `connectorConfig` from > it. Importing or re-exporting decorated entity classes as values ships them into the > browser bundle. The bundler lowers the decorators into an invalid class expression, > `vite build` still exits 0, type-check and deploy both pass, and the deployed page > renders blank with `Uncaught SyntaxError: Invalid or unexpected token`. Always use `import type` and `export type` for Category A entity classes in `rayfin/connectors//schema.ts`. ```typescript title="rayfin/connectors/inventory/schema.ts" import type { GraphQLBackedConnector } from '@microsoft/rayfin-connector-fabric-graphql'; import type { ConnectorConfig } from '@microsoft/rayfin-connectors'; import type { Order } from './Order.js'; import type { Customer } from './Customer.js'; export type { Order } from './Order.js'; export type { Customer } from './Customer.js'; export const connectorConfig = { connector: 'fabric-warehouse', operations: ['read', 'update'], entities: { Order: ['orderId', 'customerId', 'total', 'placedUtc'], Customer: ['customerId', 'email'], }, } as const satisfies ConnectorConfig; export type InventorySchema = GraphQLBackedConnector< { Order: typeof Order; Customer: typeof Customer }, typeof connectorConfig >; ``` Give `connectorConfig.entities` string arrays of entity property names rather than entity classes. The names are the generated TypeScript property names, not the source column names. ## Troubleshoot client wiring [#troubleshoot-client-wiring] | Symptom | Cause | Fix | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `Property '' does not exist on connectors` | The connector key differs between `rayfin.yml`, `AppConnectorsSchema`, the `connectors` option, or the runtime map. | Use the `rayfin.yml` `name` in every place. | | Import of `ConnectorsRayfinClient` fails to resolve | The client was imported from the stable package entry. | Import from `@microsoft/rayfin-client/experimental`. | | `Cannot find module '@microsoft/rayfin-connector-fabric-graphql'` | `connector add` scaffolds files but does not install packages. | Run the pinned `npm install` command `connector add` printed, or rebuild it from `rayfin connector types --json`. | | Deployed page is blank with `Uncaught SyntaxError: Invalid or unexpected token` | Category A entity classes were imported or re-exported as values from `schema.ts`. | Switch entity imports and re-exports to `import type` and `export type`, and use property-name arrays in `connectorConfig.entities`. | | A read throws `SELECTION_REQUIRED` | `connectorConfig.entities` is missing, so the client has no default column list. | Add string arrays of entity property names, or pass an explicit `select([...])`. | | A Kusto query fails to reach the cluster | The runtime map omitted `{ telemetry: kusto() }`, so generated routing was not injected. | Add the Kusto runtime under the exact connector name. | | A semantic-model result cannot be read | The runtime map omitted `{ salesModel: fabricSemanticModel() }`, so the Arrow response was not decoded. | Add the semantic-model runtime under the exact connector name. | ## Continue by connector type [#continue-by-connector-type] * [Fabric SQL sources](/docs/connectors/sql-sources) * [Semantic models](/docs/connectors/semantic-models) * [KQL databases](/docs/connectors/kusto) ```prompt title="Wire connectors into a Rayfin frontend" In my Rayfin project, wire existing connectors into the frontend client. Read rayfin/rayfin.yml to get the exact connector names, then import ConnectorsRayfinClient from @microsoft/rayfin-client/experimental. Create or update src/services/rayfinClient.ts so the client type parameters are , using Record for unused data or functions schemas. Import each generated connector schema type and connectorConfig from rayfin/connectors//schema.ts. Key AppConnectorsSchema and the connectors option with the exact rayfin.yml connector names. For Category B connectors, pass the second constructor argument as a runtime map keyed by the same names: use kusto() for kusto connectors and fabricSemanticModel() for fabric-semanticmodel connectors. Do not add runtime entries for Category A unless a connector-specific package requires one. For Category A schema files, keep entity classes out of the browser bundle: use import type and export type, and set connectorConfig.entities to string arrays of entity property names rather than entity class values. ``` --- --- title: "Generating entity files" description: "Generate Category A connector entity files from metadata.json, including keys, relationships, permissions, aggregate schema, and apply steps." url: https://rayfin.ai/docs/connectors/entity-generation markdown_url: https://rayfin.ai/docs/connectors/entity-generation.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/entity-generation.mdx --- # Generating entity files > Generate Category A connector entity files from metadata.json, including keys, relationships, permissions, aggregate schema, and apply steps. Generate entity files after `npx rayfin connector add` creates a Category A connector. The command writes `rayfin/connectors//metadata.json` and a placeholder `rayfin/connectors//schema.ts`, then stops. It does not emit per-table `.ts` entity files. Category A means the connector types listed on [Connectors](/docs/connectors): `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase`. For the usage surface after generation, see [Fabric SQL sources](/docs/connectors/sql-sources). ## Follow the generation workflow [#follow-the-generation-workflow] ### 1. Read `metadata.json` [#1-read-metadatajson] Open `rayfin/connectors//metadata.json`. Treat it as the only source of truth for physical columns, primary keys, foreign keys, and server-generated column markers. ### 2. Pick the tables in scope [#2-pick-the-tables-in-scope] For a full connector surface, use every table under `schemas[].tables[]`. For a subset, filter those arrays by `tableName` before writing files. Do not generate a file for a table outside the requested scope. ### 3. Write one entity file per table [#3-write-one-entity-file-per-table] Write `rayfin/connectors//.ts` for each selected table. Follow the entity contract on this page for naming, imports, fields, primary keys, generated columns, relationships, and permissions. ### 4. Replace the placeholder aggregate [#4-replace-the-placeholder-aggregate] Overwrite `rayfin/connectors//schema.ts` with the aggregate schema. It must export the entity types, the connector schema type, and the `connectorConfig` value. ### 5. Surface every warning [#5-surface-every-warning] Report missing PK metadata, missing FK metadata, skipped relationships, unknown SQL types, and server-generated columns. Do not drop warnings silently. When the source schema changes, refresh the connector metadata before regenerating. When only entity code changes, apply the generated DAB config again with `npx rayfin up connector apply` or `npx rayfin up connector apply --name `. This apply step requires a prior `npx rayfin up` so the Fabric app exists. ## Read the `metadata.json` shape [#read-the-metadatajson-shape] `metadata.json` has this shape. Field names here are the serialized metadata keys. ```typescript interface SchemaMetadata { source: string; connector: string; connectionString: string; discoveredAt: string; schemas: SchemaEntry[]; } interface SchemaEntry { schemaName: string; tables: TableEntry[]; } interface TableEntry { tableName: string; columns: ColumnEntry[]; foreignKeys?: ForeignKeyEntry[]; primaryKeyColumns?: string[]; } interface ColumnEntry { columnName: string; dataType: string; isNullable: boolean; maxLength?: number; precision?: number; scale?: number; datePrecision?: number; identity?: { seed: string; increment: string }; default?: string; computed?: string; serverManaged?: 'rowversion' | 'temporalRowStart' | 'temporalRowEnd'; } interface ForeignKeyEntry { constraintName: string; columnName: string; referencedTableSchema: string; referencedTableName: string; referencedColumnName: string; } ``` `primaryKeyColumns` is in database `ORDINAL_POSITION` order. Preserve that order in `Source({ primaryKey })` and by-key examples. Multiple `foreignKeys` rows that share a `constraintName` form one composite foreign key. Generate one relationship for the group, not one relationship per column. Absence means unknown or not exposed. It is never permission to synthesize a key or relationship. Fabric SQL Database generally exposes PK/FK and server-generation metadata. Warehouse and Lakehouse may omit catalog metadata, and Lakehouse commonly omits PK/FK constraints entirely. ## Name entity files consistently [#name-entity-files-consistently] Derive the initial class name from the table name: `pascalCase(table.tableName)`. The file name is `.ts`, and each file contains one entity class. Pluralization is allowed when it is idempotent. Pluralizing `Order` to `Orders` is fine if that is the desired GraphQL type, but an already plural source table such as `Orders`, `Categories`, or `sales_line_items` must not become double-pluralized. Use the same final name everywhere: the class, file, `@entity()` name when supplied, `TSchema` key, re-export, and `client.connectors..` path. GraphQL type names are global across every connector in the app. Before finalizing a name, scan other `rayfin/connectors/*/` directories and the entities already generated for this connector. If a name collides, prefix it with the PascalCased source database name from `metadata.json` `source`. If that still collides, prefix it with the PascalCased connector name from `rayfin.yml`. Disambiguate only colliding names; never prefix every entity. Report each rename to the user. ## Declare primary keys from metadata [#declare-primary-keys-from-metadata] When `table.primaryKeyColumns` is present and non-empty, resolve each SQL column name against `table.columns` before converting it to the TypeScript property name. Declare exactly those property names in `Source({ primaryKey })`, preserving metadata order. ```typescript title="rayfin/connectors/sales/OrderItem.ts" import { entity, int, uuid, Source } from '@microsoft/rayfin-core/experimental'; @entity() export class OrderItem extends Source({ schema: 'dbo', table: 'OrderItem', primaryKey: ['orderId', 'productId'], }) { @uuid({ column: 'OrderID' }) orderId!: string; @uuid({ column: 'ProductID' }) productId!: string; @int() quantity!: number; } ``` A primary-key column must exist and be non-nullable. If metadata names a missing or nullable key column, stop generation for that table and report the inconsistency. Do not choose a replacement column, do not rename the PK to `id`, and do not infer uniqueness from sampled values. When `primaryKeyColumns` is absent or empty, emit `primaryKey: []`. The entity is keyless and exposes no `findByKey`, `update`, or `delete`. Lakehouse SQL endpoints commonly fall into this case. The builder may manually add a logical key later when they know the source contract, but an agent must not add one without explicit input. ## Map SQL column types to decorators [#map-sql-column-types-to-decorators] Look up `column.dataType` case-insensitively. | SQL type family | Decorator | TypeScript type | | -------------------------------------------------------------------------- | -------------------------------- | --------------- | | `int`, `bigint`, `smallint`, `tinyint` | `@int()` | `number` | | `decimal`, `numeric`, `money`, `smallmoney`, `float`, `real` | `@decimal({ precision, scale })` | `number` | | `bit` | `@boolean()` | `boolean` | | `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset`, `time` | `@date()` | `Date` | | `uniqueidentifier` | `@uuid()` | `string` | | `varchar`, `nvarchar`, `char`, `nchar`, `text`, `ntext` | `@text()` | `string` | | Anything else, such as `geography`, `hierarchyid`, `xml`, or vector types | `@text()` | `string` | For the fallback case, emit a warning such as: ```text Unknown SQL type geography for Location.Shape; falling back to @text(). ``` If the entity class name equals a field's TypeScript type, qualify the field type with `globalThis`. For example, a table named `Date` with a `datetime2` column uses `globalThis.Date` for that field annotation so the type does not resolve to the class. ## Mark server-generated columns [#mark-server-generated-columns] A column is server-generated when it has any of these metadata markers: * `identity` for an `IDENTITY(seed, increment)` column. * `default` for a `DEFAULT` expression. * `computed` for a computed column declared with `AS (...)`. * `serverManaged` for rowversion or temporal-period columns. Wrap the TypeScript type in `AutoGenerated` when any marker is present. The decorator and its `column:` option do not change, and there is no decorator option for this metadata. Do not invent one. `AutoGenerated` makes the column optional on create and update input, and reads it back as plain `T`. Most generated values still cannot be written: `IDENTITY`, computed, rowversion, and temporal values are rejected by the source if supplied. A plain `DEFAULT` column may be omitted for the default or supplied to override it. Surface a one-line note for each server-generated column, such as: ```text Column 'Order.OrderID' is server-generated (identity); the source will populate it, so omit it on create(). ``` A server-generated key still follows the normal primary-key rules. Being generated does not make a nullable key valid and does not remove the key from `Source({ primaryKey })`. ## Build field decorator options [#build-field-decorator-options] Generate each column property as `camelCase(column.columnName)`. Nullable columns use `?:`; non-nullable columns use `!:`. Build the decorator option object in this order, omitting keys that do not apply: 1. `optional: true` when `column.isNullable` is true. 2. `column: ''` when the SQL column name differs from the TypeScript property name. Escape embedded single quotes. 3. `max: ` for `@text()` when `maxLength > 0`. 4. `precision:

, scale: ` for `@decimal()` when both values are present. Do not emit integer `min` or `max` from SQL precision; those options are value bounds, not storage capacity. If the option object is empty, write `@text()`, not `@text({})`. ## Generate relationships only from foreign keys [#generate-relationships-only-from-foreign-keys] Generate forward `@one` and reverse `@many` relationships from FK metadata only. Do not infer relationships from matching column names, star-schema patterns, or sampled values. Group each table's `foreignKeys` by `constraintName` before generating relationships. A group is one FK relationship, including composite FKs. Preserve the row order inside the group. For each forward `@one` on the referencing table: * Every FK row in the group must reference the same schema and table. If not, report the inconsistent metadata and skip that constraint. * `fieldName = camelCase(singularize(referencedTableName))`. * If more than one constraint would produce the same field name, derive a stable disambiguated name from `constraintName`. * `sourceFields = group.map(fk => camelCase(fk.columnName))`. * `targetFields = group.map(fk => camelCase(fk.referencedColumnName))`. * If any source column is nullable, mark the relationship optional with `{ optional: true }` and `?:`. ```typescript @one(() => Customer, { sourceFields: ['customerId'], targetFields: ['customerId'] }) customer!: Customer; @one(() => SalesRep, { optional: true, sourceFields: ['salesRepId'], targetFields: ['salesRepId'], }) salesRep?: SalesRep; ``` Self-referencing FKs follow the same rules and must be optional. Use the current class in the resolver and add no sibling import. The entity and DAB relationship generate, but the client cannot select a dotted path across a self-relationship. If the referenced table is missing from metadata, skip the relationship and warn. In subset mode, also skip a relationship whose target table exists in metadata but is outside the selected set. Never import a sibling file you did not write. Build reverse `@many` relationships from an index of grouped FKs across all selected tables. Every grouped FK from another table to the current table becomes one reverse relationship: * `fieldName = camelCase(pluralize(otherTable.tableName))`. * `sourceFields = group.map(fk => camelCase(fk.referencedColumnName))`. * `targetFields = group.map(fk => camelCase(fk.columnName))`. * In subset mode, emit `@many` only when the referencing table is also selected. ```typescript @many(() => OrderItem, { sourceFields: ['productId'], targetFields: ['productId'] }) orderItems!: OrderItem[]; ``` Use the simplified naming rules from the generator. `singularize`: `ies` becomes `y` when length is greater than three; `xes`, `ses`, `ches`, and `shes` drop `es`; a non-`s` word ending in `s` drops the final `s`; everything else is unchanged. `pluralize` is idempotent: if the name already ends in `s`, `es`, or `ies`, leave it unchanged; otherwise a non-vowel `y` becomes `ies`, `x`, `z`, `ch`, and `sh` add `es`, and other names add `s`. ## Report missing metadata [#report-missing-metadata] When `primaryKeyColumns` is absent or empty, warn: ```text No PK metadata available for ; generated as a keyless entity. ``` When `foreignKeys` is absent or empty and no reverse FK points at the table, warn: ```text No FK metadata available for ; relationships omitted. ``` For Lakehouse, these warnings describe a known metadata limitation. Keep the generated entity keyless and relationship-free unless the builder explicitly supplies logical keys or relationships. ## Build imports deterministically [#build-imports-deterministically] Each entity file imports from `@microsoft/rayfin-core/experimental`. * Always include `entity` and `Source`. * Then append used field and relationship decorators in this order: `boolean`, `date`, `decimal`, `int`, `text`, `uuid`, `one`, `many`. * If any column is server-generated, add `import type { AutoGenerated } from '@microsoft/rayfin-core/experimental';`. * Import each non-self relationship target as a sibling value import, alphabetized: `import { Category } from './Category.js';`. * In subset mode, only surviving relationships contribute imports. ## Use the canonical entity pattern [#use-the-canonical-entity-pattern] ```typescript title="rayfin/connectors/inventory/Product.ts" import { entity, date, int, many, one, text, uuid, Source, } from '@microsoft/rayfin-core/experimental'; import type { AutoGenerated } from '@microsoft/rayfin-core/experimental'; import { Category } from './Category.js'; import { OrderItem } from './OrderItem.js'; @entity() export class Product extends Source({ schema: 'dbo', table: 'Product', primaryKey: ['productId'], }) { @uuid({ column: 'ProductID' }) productId!: string; @text() name!: string; @int() stock!: number; @date({ column: 'CreatedUtc' }) createdUtc!: AutoGenerated; @one(() => Category, { sourceFields: ['categoryId'], targetFields: ['categoryId'] }) category!: Category; @many(() => OrderItem, { sourceFields: ['productId'], targetFields: ['productId'] }) orderItems!: OrderItem[]; } ``` ## Scope `@role()` to connector operations [#scope-role-to-connector-operations] Add `@role()` to connector entities the same way you secure Rayfin data entities. Legal actions are `'read'`, `'create'`, `'update'`, `'delete'`, and `'*'`. The actions on every entity must be a subset of the connector's YAML `operations:`. The settings validator does not catch a mismatch today; DAB fails when `rayfin up connector apply` runs. Narrow YAML first, mirror the same connector-wide list into `connectorConfig.operations`, then add entity decorators that grant only the actions that entity needs. Stack multiple `@role()` decorators when different roles need different actions. ```yaml title="rayfin/rayfin.yml" connectors: - name: inventory type: fabric-warehouse version: '1' operations: ['read', 'update'] ``` ```typescript title="rayfin/connectors/inventory/Order.ts" import { role } from '@microsoft/rayfin-core'; import { entity, decimal, text, uuid, Source } from '@microsoft/rayfin-core/experimental'; @role('authenticated', ['read', 'update']) @entity() export class Order extends Source({ schema: 'dbo', table: 'Order', primaryKey: ['orderId'] }) { @uuid({ column: 'OrderID' }) orderId!: string; @text() customerEmail!: string; @decimal({ precision: 18, scale: 2 }) total!: number; } ``` ## Write row-level policies with the typed DSL [#write-row-level-policies-with-the-typed-dsl] Policies use the shared typed `claims` and `item` DSL. Never write raw SQL or DAB policy strings in connector entities. The available claims are `claims.sub`, `claims.email`, and `claims.role`; item fields are addressed as `item.` using the entity property name. Use `.eq(...)`, `.and(...)`, and `.or(...)` to combine conditions. `RoleDeclarationOptions` also accepts `include` and `exclude` field lists. Inspect `metadata.json` for ownership columns such as `owner_id`, `user_id`, `tenant_id`, or `created_by`. If one is present, ask whether rows should be scoped per signed-in user. See [Permissions and row-level security](/docs/data/permissions) for the full shared policy DSL. ```typescript title="rayfin/connectors/inventory/Document.ts" import { role } from '@microsoft/rayfin-core'; import { entity, text, uuid, Source } from '@microsoft/rayfin-core/experimental'; @role('authenticated', ['read', 'update'], { policy: (claims, item) => claims.sub.eq(item.owner_id), exclude: ['internalNotes'], }) @entity() export class Document extends Source({ schema: 'dbo', table: 'Document', primaryKey: ['id'] }) { @uuid() id!: string; @text({ max: 128 }) owner_id!: string; @text({ max: 200 }) title!: string; @text({ optional: true, max: 4000 }) internalNotes?: string; } ``` ## Export the aggregate schema [#export-the-aggregate-schema] The aggregate `rayfin/connectors//schema.ts` must export three things: 1. Entity re-exports as types, using `export type { Entity }`. 2. The `Schema` type, where `` is the PascalCase connector name plus `Schema`. 3. The `connectorConfig` value declared with `as const satisfies ConnectorConfig`. `GraphQLBackedConnector` is the published Category A marker. Do not invent per-type names such as `FabricWarehouse` or `FabricSqlAnalytics`; they are not exported markers. Use `as const satisfies ConnectorConfig`, not a `: ConnectorConfig` annotation, so the `connector` and `operations` literals survive. The marker reads those literals to expose the right methods and dialect-specific return types. ```typescript title="rayfin/connectors/inventory/schema.ts" import type { GraphQLBackedConnector } from '@microsoft/rayfin-connector-fabric-graphql'; import type { ConnectorConfig } from '@microsoft/rayfin-connectors'; import type { Customer } from './Customer.js'; import type { Order } from './Order.js'; import type { OrderItem } from './OrderItem.js'; export type { Customer } from './Customer.js'; export type { Order } from './Order.js'; export type { OrderItem } from './OrderItem.js'; export const connectorConfig = { connector: 'fabric-warehouse', operations: ['read', 'update'], entities: { Customer: ['customerId', 'email', 'displayName'], Order: ['orderId', 'customerId', 'customerEmail', 'total', 'placedUtc'], OrderItem: ['orderId', 'productId', 'quantity', 'unitPrice'], }, } as const satisfies ConnectorConfig; export type InventorySchema = GraphQLBackedConnector< { Customer: typeof Customer; Order: typeof Order; OrderItem: typeof OrderItem; }, typeof connectorConfig >; ``` The `entities` map is keyed exactly like `TSchema`. List scalar entity property names, not database column names; if a field declares `graphqlName`, list that GraphQL field name. Leave relationship fields out. > [!WARNING] > In `schema.ts`, use `import type` and `export type` for entity classes. A value import or > value re-export ships decorated classes to the browser bundle, and the deployed page can > render blank even when type-checking and build commands pass. ## Avoid Category A anti-patterns [#avoid-category-a-anti-patterns] * Never leave the placeholder `schema.ts` as bare re-exports; app code needs both `Schema` and `connectorConfig`. * Never import or re-export entity classes as values in `schema.ts`; use `import type` and `export type`. * Populate `connectorConfig.entities` with property-name arrays, not entity classes. Omitting it makes no-selection reads throw `SELECTION_REQUIRED`; using classes brings decorated classes into the browser bundle. * In subset mode, list only generated entities in `TSchema`. * Keep `connectorConfig.operations` identical to YAML `operations:`, and keep every entity `@role()` action a subset of that connector-wide list. * Never widen a decorator to match the full connector operation list when the entity should be narrower. * Use the typed policy DSL; never raw SQL or DAB policy strings. * Never double-pluralize entity or relationship names. * Disambiguate duplicate GraphQL type names only when they collide; never blanket-prefix. * Treat `metadata.json` as the only source of truth for keys and relationships. * Never synthesize a primary key or infer a relationship from column names, sampled values, or naming conventions. * When the user asks for one entity, filter `metadata.json` and generate that subset; do not regenerate every table. * Never edit `metadata.json` or `dab-config.json` by hand; both are regenerated artifacts. ## Troubleshoot generation and apply failures [#troubleshoot-generation-and-apply-failures] | Symptom | Likely cause | Fix | | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rayfin up connector apply` fails on a role action | An entity `@role()` includes an action that is not in YAML `operations:` | Narrow the decorator or YAML so the entity action set is a subset. | | `rayfin up connector apply` fails with duplicate or redefined GraphQL type | Two connectors generated the same entity name | Prefix the colliding entity with the source database name, or connector name if needed, and update the class, file, `@entity()` name, `TSchema` key, re-export, and access path. | | `rayfin connector add` writes YAML but no entity files | Expected; the CLI writes metadata and a placeholder only | Generate entity files from `metadata.json`. If metadata is missing, schema discovery failed. | | `Property '' does not exist on connectors` | The connector key in `AppConnectorsSchema` differs from the `connectors` option or YAML name | Use the `rayfin.yml` connector `name` in all three places. | | A CRUD method is missing from autocomplete | Expected; `Schema` narrows methods to `operations:` and removes by-key methods from keyless entities | Check YAML, `connectorConfig.operations`, and `Source({ primaryKey })`. | | `ConnectorsRayfinClient` import fails | It was imported from the stable client entry | Import it from `@microsoft/rayfin-client/experimental`. | | `Cannot find module '@microsoft/rayfin-connector-fabric-graphql'` | Connector packages were not installed | Run the pinned install command printed by `connector add`, or reconstruct it from `npx rayfin connector types --json`. | | Deployed page is blank with a syntax error, though build and deploy passed | `schema.ts` imported or re-exported decorated entity classes as values | Change entity imports and re-exports to `import type` and `export type`, and keep `connectorConfig.entities` as arrays. | | A read throws `SELECTION_REQUIRED` | `connectorConfig.entities` is missing and no explicit selection was passed | Add scalar property names to `entities`, or pass an explicit selection. | | A read returns null or errors for a field that exists in the source | `entities` lists database column names rather than entity property names | Use the names declared on the generated class, such as `productId`, not `ProductID`. | ```prompt title="Generate Category A connector entities" In my Rayfin project, generate entity files for the existing Category A connector named inventory. Read rayfin/connectors/inventory/metadata.json first. Ask me which tables are in scope if I have not already named them; otherwise filter schemas[].tables[] by tableName. For each selected table, write rayfin/connectors/inventory/.ts using Source({ schema, table, primaryKey }) from @microsoft/rayfin-core/experimental, with primary keys, SQL type mappings, AutoGenerated server-generated columns, and relationships derived only from metadata. Add @role() decorators whose actions are a subset of the connector YAML operations, and ask whether ownership columns such as owner_id, user_id, tenant_id, or created_by should become row-level policies. Then overwrite rayfin/connectors/inventory/schema.ts with type-only entity imports and exports, an InventorySchema type using GraphQLBackedConnector, and connectorConfig declared as const satisfies ConnectorConfig with operations and an entities map of scalar property names. Surface every warning, then run npx rayfin up connector apply --name inventory after a prior npx rayfin up exists. ``` --- --- title: "Connectors" description: "Read and write existing Microsoft Fabric data — warehouses, SQL databases, Lakehouse SQL endpoints, semantic models, and KQL databases — from a Rayfin app." url: https://rayfin.ai/docs/connectors markdown_url: https://rayfin.ai/docs/connectors.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/index.mdx --- # Connectors > Read and write existing Microsoft Fabric data — warehouses, SQL databases, Lakehouse SQL endpoints, semantic models, and KQL databases — from a Rayfin app. Connectors let a Rayfin app query data it does not own. Your [entities](/docs/data) live in the database Rayfin provisions for you; a connector points at data that already exists somewhere else in Microsoft Fabric — a Warehouse, a SQL Database, a Lakehouse SQL analytics endpoint, a Power BI semantic model, or a KQL database — and exposes it through the same client. > [!WARNING] > Connectors are in private preview. The `rayfin connector` command group is hidden until > you opt in, `ConnectorsRayfinClient` ships from an `/experimental` subpath, and the API may > change between releases. Confirm the feature is available in your own tenant before you > design an app around it. ## The two categories [#the-two-categories] Which commands apply, and what your app code looks like, depends on the connector's category. | | Category A — entity connectors | Category B — query connectors | | ---------------------- | --------------------------------------------------------------- | -------------------------------------------- | | **Types** | `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase` | `fabric-semanticmodel`, `kusto` | | **App surface** | `client.connectors..` — typed CRUD | `client.connectors..executeQuery(...)` | | **You write** | Entity classes with `@role()` policies | A DAX or KQL query string | | **Row-level security** | Yes, via `@role()` policies | No — the source enforces its own | | **Schema discovery** | Yes — `metadata.json` | No | | **Auth** | `delegated` or `application` | `delegated` only | Category A turns Fabric SQL into typed entities that behave like your own — see [Fabric SQL sources](/docs/connectors/sql-sources). Category B hands a raw query to a platform-managed function and returns a table — see [Semantic models](/docs/connectors/semantic-models) and [KQL databases](/docs/connectors/kusto). ## Connector types [#connector-types] | Type | Fabric item | Category | Operations | Auth | | ---------------------- | -------------- | -------- | ------------------------------------ | -------------------------- | | `fabric-sqlanalytics` | Lakehouse | A | `read` | `delegated`, `application` | | `fabric-warehouse` | Warehouse | A | `read`, `create`, `update`, `delete` | `delegated`, `application` | | `fabric-sqldatabase` | SQL Database | A | `read`, `create`, `update`, `delete` | `delegated`, `application` | | `fabric-semanticmodel` | Semantic model | B | `executeQuery` | `delegated` | | `kusto` | KQL Database | B | `executeQuery`, `executeCommand` | `delegated` | Lakehouse SQL analytics endpoints are read-only at the source, so `fabric-sqlanalytics` allows only `read`. Category B types are pinned to an adapter version (`version: '1'` today) and are delegated-only — `rayfin up` rejects `auth.type: application` on them. See [Connector authentication](/docs/connectors/auth). Run `npx rayfin connector types --json` to print the live catalog, including the exact client packages and version to install for each type. ## Enable connectors [#enable-connectors] The `connector` command group is registered only when the project opts in. Prefer the declarative setting: ```yaml title="rayfin/rayfin.yml" services: connectors: enabled: true ``` Two other things also turn it on: a non-empty `connectors:` block in `rayfin.yml` (which `connector add` writes, so the feature is self-sustaining after the first connector), and the environment variable for a single command: ```bash RAYFIN_FEATURE_FLAGS=connectors npx rayfin connector types ``` With none of the three, the CLI reports an unknown command. ## How a connector reaches your app [#how-a-connector-reaches-your-app] ```mermaid flowchart LR Dev(["Developer"]) -->|"connector add"| YML["rayfin.yml
+ rayfin/connectors/<name>/"] YML -->|"rayfin up"| App subgraph App["Fabric app"] Web["WebService"] end subgraph Sources["Existing Fabric data"] SQL[("Warehouse · SQL DB
Lakehouse endpoint")] Model["Semantic model"] KQL[("KQL Database")] end User(["Signed-in user"]) ==> Web Web -.->|"delegated token"| SQL Web -.->|"delegated token"| Model Web -.->|"delegated token"| KQL class Dev,User actor class Web,YML service class SQL,KQL store class Model external ``` Nothing about the source reaches the browser. The workspace and item IDs live in `rayfin.yml` and are injected server-side, so a client-side query carries only the query itself. ## Where connector state lives [#where-connector-state-lives] | Path | Written by | Contents | | ---------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------- | | `rayfin/rayfin.yml` | `connector add` | The `connectors:` block — `name`, `type`, `version`, `config`, `auth`, `operations` | | `rayfin/connectors//metadata.json` | `connector add` | Discovered schema. Category A only. Never edit by hand | | `rayfin/connectors//schema.ts` | `connector add`, then you | The typed marker and `connectorConfig` your app imports | `connector add` scaffolds a **placeholder** `schema.ts` for Category A. You replace it with the aggregate schema after generating entity files — see [Generating entity files](/docs/connectors/entity-generation). For Category B the generated `schema.ts` is complete and must not be edited. ## The workflow [#the-workflow] ### 1. Find the source [#1-find-the-source] `npx rayfin connector search` lists the Fabric items the signed-in identity can add, with a ready-to-run `add` command for each. ### 2. Add it [#2-add-it] `npx rayfin connector add --type --workspace-id --item-id ` writes the `rayfin.yml` entry and scaffolds the connector directory. See [Adding a connector](/docs/connectors/adding). ### 3. Install the packages [#3-install-the-packages] `connector add` scaffolds files but installs nothing. Run the version-pinned `npm install` it prints, verbatim. ### 4. Build the typed surface [#4-build-the-typed-surface] Category A: generate entity files from `metadata.json` and write the aggregate `schema.ts`. Category B: the generated `schema.ts` is already complete. ### 5. Wire the client [#5-wire-the-client] Expose the connector as `client.connectors.` through `ConnectorsRayfinClient` — see [Wiring connectors into your app](/docs/connectors/client-setup). ### 6. Deploy [#6-deploy] `npx rayfin up` deploys the connector alongside the rest of the app. ## In this section [#in-this-section] * **[Adding a connector](/docs/connectors/adding)** — `search`, `add`, `list`, `remove`, and the `rayfin.yml` entry they produce. * **[Wiring connectors into your app](/docs/connectors/client-setup)** — `ConnectorsRayfinClient`, the runtime map, and the bundling trap to avoid. * **[Fabric SQL sources](/docs/connectors/sql-sources)** — reading and writing Category A entities, and what each dialect returns. * **[Generating entity files](/docs/connectors/entity-generation)** — turning `metadata.json` into entity classes with row-level policies. * **[Semantic models](/docs/connectors/semantic-models)** — running DAX and reading the result. * **[KQL databases](/docs/connectors/kusto)** — running KQL queries and management commands. * **[Connector authentication](/docs/connectors/auth)** — `delegated` versus `application`, and the permissions each needs. ```prompt title="Connect a Rayfin app to existing Fabric data" In my Rayfin project, connect to an existing Microsoft Fabric data source (ask me which one — a Warehouse, SQL Database, Lakehouse SQL analytics endpoint, semantic model, or KQL database, and ask me for its workspace ID and item ID rather than inventing them). First enable the feature by adding services.connectors.enabled: true to rayfin/rayfin.yml. Then run `npx rayfin connector add --type --workspace-id --item-id ` and run the version-pinned npm install command it prints, verbatim — do not drop the version. If it is a Category A type (fabric-sqlanalytics, fabric-warehouse, fabric-sqldatabase), generate entity files from rayfin/connectors//metadata.json and overwrite the placeholder schema.ts with the aggregate schema. If it is Category B (fabric-semanticmodel, kusto), the generated schema.ts is already complete — do not edit it. Finally, wire it up as client.connectors. with ConnectorsRayfinClient imported from @microsoft/rayfin-client/experimental, and deploy with `npx rayfin up`. ``` --- --- title: "KQL databases" description: "Run KQL queries and Kusto management commands against a Fabric KQL Database from a Rayfin app." url: https://rayfin.ai/docs/connectors/kusto markdown_url: https://rayfin.ai/docs/connectors/kusto.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/kusto.mdx --- # KQL databases > Run KQL queries and Kusto management commands against a Fabric KQL Database from a Rayfin app. Run KQL against an existing Fabric KQL Database when your app needs operational or telemetry data owned by Fabric. The `kusto` connector is a [Category B function-bridge connector](/docs/connectors): it exposes `executeQuery` for KQL, `executeCommand` for management commands, uses delegated authentication only, and is pinned to an adapter version (`version: '1'` today). > [!WARNING] > `kusto` is a private-preview connector type. `rayfin up` rejects > `auth.type: application`; keep the connector delegated so each query or command runs as > the signed-in user. ## Keep the generated schema [#keep-the-generated-schema] `npx rayfin connector add --type kusto` resolves the KQL Database routing values from the Fabric `(workspaceId, itemId)` pair and writes them into `rayfin/connectors//schema.ts`. The generated file is complete for this connector type: it exports the phantom marker used in `AppConnectorsSchema` and the Kusto-specific `connectorConfig`. ```typescript title="rayfin/connectors/telemetry/schema.ts" // @generated — do not edit. import type { Kusto, KustoConnectorConfig } from '@microsoft/rayfin-connector-kusto'; export type TelemetrySchema = Kusto<'executeQuery' | 'executeCommand'>; export const connectorConfig = { connector: 'kusto', queryServiceUri: 'https://.kusto.fabric.microsoft.com', databaseName: '', } as const satisfies KustoConnectorConfig; ``` Do not hand-edit this file. If `queryServiceUri` or `databaseName` looks wrong, regenerate it by removing and adding the connector again: ```bash npx rayfin connector remove telemetry npx rayfin connector add --type kusto --workspace-id --item-id --name telemetry ``` `queryServiceUri` and `databaseName` live only in the generated `schema.ts`. They are not part of the `rayfin/rayfin.yml` schema, must not be written into `rayfin.yml`, and must not be sent from app code. ## Register the runtime [#register-the-runtime] Register `kusto()` in the connector runtime map before calling the connector. The runtime injects the generated `queryServiceUri` and `databaseName` after caller input, so a caller cannot override the cluster routing. Without the runtime map, nothing injects that routing and the connector cannot reach the cluster. See [Wiring connectors into your app](/docs/connectors/client-setup) for the full client setup. ```typescript title="src/services/rayfinClient.ts" import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental'; import { kusto } from '@microsoft/rayfin-connector-kusto'; import type { AppSchema } from '../../rayfin/data/schema'; import type { TelemetrySchema } from '../../rayfin/connectors/telemetry/schema'; import { connectorConfig as telemetryConfig } from '../../rayfin/connectors/telemetry/schema'; type AppConnectorsSchema = { telemetry: TelemetrySchema; }; export const client = new ConnectorsRayfinClient< AppSchema, Record, AppConnectorsSchema >( { baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, connectors: { telemetry: telemetryConfig, }, }, { telemetry: kusto(), } ); ``` The key must match the connector `name` in `rayfin.yml`, the `AppConnectorsSchema` property, the `connectors` option, and the runtime map. ## Generate a client request id [#generate-a-client-request-id] Kusto correlation travels outside the response body. Generate a `clientRequestId`, pass it to the operation, and pass the same value to `toQueryResult`. The connector forwards it as the `x-ms-client-request-id` header. If you omit it, the runtime generates one for the Kusto request, but your caller cannot correlate the normalized result because the native response body does not carry the id. ```typescript const clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`; ``` Normalized results echo the `clientRequestId` you give to `toQueryResult` and may carry an `activityId` when one is available. ## Call `executeQuery` [#call-executequery] `executeQuery` accepts a KQL query and resolves to the native Kusto v1 `{ Tables }` document. The connector function is a byte pump: it relays the Kusto response untouched, so the caller must normalize it with `toQueryResult(response, { clientRequestId })`. ```typescript title="src/features/telemetry/loadErrorsByState.ts" import { toQueryResult } from '@microsoft/rayfin-connector-kusto'; import { client } from '../../services/rayfinClient'; export async function loadErrorsByState() { const clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`; const response = await client.connectors.telemetry.executeQuery({ query: ` AppEvents | where Severity == "Error" | summarize ErrorCount = count() by State | top 10 by ErrorCount desc `, clientRequestId, }); const result = toQueryResult(response, { clientRequestId }); if (result.status === 'error') { throw new Error( `${result.error.message} Client request id: ${result.clientRequestId}` ); } return result.tables.flatMap((table) => table.rows.map((row) => { const record = Object.fromEntries( table.columns.map((column, index) => [column.name, row[index]]) ); return { state: String(record.State ?? ''), errorCount: Number(record.ErrorCount ?? 0), tableName: table.name, clientRequestId: result.clientRequestId, }; }) ); } ``` ## Call `executeCommand` [#call-executecommand] `executeCommand` runs a Kusto management command. The command text starts with a leading dot and routes to the management endpoint. It returns the same native Kusto v1 `{ Tables }` document as `executeQuery`, so normalize it the same way. ```typescript title="src/features/telemetry/showDatabases.ts" import { toQueryResult } from '@microsoft/rayfin-connector-kusto'; import { client } from '../../services/rayfinClient'; export async function showDatabases() { const clientRequestId = `KPC.rayfin_kusto_v1;${crypto.randomUUID()}`; const response = await client.connectors.telemetry.executeCommand({ command: '.show databases', clientRequestId, }); const result = toQueryResult(response, { clientRequestId }); if (result.status === 'error') { throw new Error(result.error.message); } return result.tables; } ``` Use `executeCommand` only for Kusto management commands. Use `executeQuery` for KQL query text. ## Normalize the Kusto table shape [#normalize-the-kusto-table-shape] `KustoOperationCatalog.executeQuery` is typed as `OperationDef`, so the operation returns the native wire document. `toQueryResult` converts that document into a discriminated union: ```typescript type KustoQueryResult = | { status: 'success'; tables: KustoTable[]; clientRequestId: string; activityId?: string; } | { status: 'error'; error: { message: string; code?: string; }; clientRequestId: string; activityId?: string; }; type KustoTable = { name: string; columns: { name: string; type: string }[]; rows: unknown[][]; }; ``` Rows are row-major arrays aligned with `columns`. A query can return more than one table, so render or inspect every entry in `result.tables`. ```typescript title="src/features/telemetry/KustoTables.tsx" import type { KustoQueryResult } from '@microsoft/rayfin-connector-kusto'; export function KustoTables({ result }: { result: KustoQueryResult }) { if (result.status === 'error') { return

{result.error.message}

; } return ( <> {result.tables.map((table) => (

{table.name}

{table.columns.map((column) => ( ))} {table.rows.map((row, rowIndex) => ( {table.columns.map((column, columnIndex) => ( ))} ))}
{column.name}
{String(row[columnIndex] ?? '')}
))} ); } ``` ## Exercise the connector from the CLI [#exercise-the-connector-from-the-cli] `connector inspect` does not support `kusto`; it errors with `Unsupported connector type: kusto`. There is no ad-hoc query path for Kusto today, so `connector invoke` is the development loop. ```bash npx rayfin up npx rayfin connector invoke telemetry executeQuery --input '{"query":"AppEvents | take 10"}' npx rayfin connector invoke telemetry executeCommand --input '{"command":".show databases"}' ``` Unlike `fabric-semanticmodel`, Kusto `connector invoke` POSTs to the deployed item, so a real query requires a deployed backend (`rayfin up`). A resolved invocation is not automatically a success: a connector returning the raw envelope can report `status: 'Failed'`, and the CLI exits non-zero for that failure. See [CLI connector reference](/docs/reference/cli/connector) for payload rules and error handling. ## Troubleshoot KQL database queries [#troubleshoot-kql-database-queries] | Symptom | Likely cause | Fix | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | `rayfin up` rejects `auth.type: application` | Category B connectors are delegated-only. | Set `auth.type: delegated`. | | `executeQuery` or `executeCommand` cannot reach the cluster. | `kusto()` is missing from the runtime map, so routing was not injected. | Register `{ telemetry: kusto() }` as the client's second constructor argument. | | A caller tries to pass `queryServiceUri` or `databaseName`. | Those are connector-owned fields injected from generated config after caller input. | Remove them from app code and re-add the connector if the generated values are wrong. | | `connector inspect` reports `Unsupported connector type: kusto`. | The inspect command has no Kusto path. | Use `npx rayfin connector invoke telemetry executeQuery --file ./query.json` after deploying. | | `connector invoke` reports `No remote endpoint configured`. | Kusto invoke uses the deployed item transport. | Run `npx rayfin up`, then invoke again. | | `toQueryResult` returns `clientRequestId: ''`. | The caller did not pass the generated id to `toQueryResult`. | Reuse the same `clientRequestId` for the operation and normalization call. | | `client.connectors.telemetry` is not typed. | The connector key differs across `rayfin.yml`, `AppConnectorsSchema`, the `connectors` option, or the runtime map. | Use the connector `name` from `rayfin.yml` in all four places. | ```prompt title="Query a Fabric KQL Database from a Rayfin app" In my Rayfin project, add a KQL Database connector named telemetry and call it from the frontend. Use `npx rayfin connector add --type kusto --workspace-id --item-id --name telemetry`, run the pinned npm install command the CLI prints, and do not edit rayfin/connectors/telemetry/schema.ts by hand. Do not put queryServiceUri or databaseName in rayfin/rayfin.yml or pass them from app code. Wire `TelemetrySchema` into `AppConnectorsSchema`, pass `connectorConfig` in the ConnectorsRayfinClient `connectors` option, and register `{ telemetry: kusto() }` in the runtime map. For a KQL query, create a client request id in the `KPC.rayfin_kusto_v1;` format, pass it to `client.connectors.telemetry.executeQuery({ query, clientRequestId })`, then normalize with `toQueryResult(response, { clientRequestId })` and branch on `result.status`. Use `executeCommand` only for Kusto management commands whose text starts with a leading dot. ``` --- --- title: "Semantic models" description: "Run DAX against a Fabric semantic model from a Rayfin app and handle typed table results, row limits, and connector diagnostics." url: https://rayfin.ai/docs/connectors/semantic-models markdown_url: https://rayfin.ai/docs/connectors/semantic-models.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/semantic-models.mdx --- # Semantic models > Run DAX against a Fabric semantic model from a Rayfin app and handle typed table results, row limits, and connector diagnostics. Run DAX against an existing Fabric semantic model when your app needs measures or model logic that already lives in Power BI. The `fabric-semanticmodel` connector is a [Category B function-bridge connector](/docs/connectors): it exposes `executeQuery`, uses delegated authentication only, and is pinned to an adapter version (`version: '1'` today). > [!WARNING] > `fabric-semanticmodel` is a private-preview connector type. `rayfin up` rejects > `auth.type: application`; keep the connector delegated so each query runs as the > signed-in user. ## Keep the generated schema [#keep-the-generated-schema] `npx rayfin connector add --type fabric-semanticmodel` writes `rayfin/connectors//schema.ts`. The file is complete for this connector type: it exports the phantom marker used in `AppConnectorsSchema` and a generic `connectorConfig` that tells the client which runtime to use. ```typescript title="rayfin/connectors/salesModel/schema.ts" // @generated — do not edit. import type { ConnectorConfig } from '@microsoft/rayfin-connectors'; import type { FabricSemanticModel } from '@microsoft/rayfin-connector-fabric-semanticmodel'; export type SalesModelSchema = FabricSemanticModel<'executeQuery'>; export const connectorConfig = { connector: 'fabric-semanticmodel', } as const satisfies ConnectorConfig; ``` Do not hand-edit this file. If the connector points at the wrong semantic model, regenerate it by removing and adding the connector again: ```bash npx rayfin connector remove salesModel npx rayfin connector add --type fabric-semanticmodel --workspace-id --item-id --name salesModel ``` The workspace and item IDs belong under the connector's `config:` entry in `rayfin/rayfin.yml`; the app never sends them from the browser. ## Register the runtime [#register-the-runtime] Register `fabricSemanticModel()` in the connector runtime map before calling the connector. The runtime decodes the Arrow response and normalizes the operation output; without it, `executeQuery` does not return the shape its TypeScript marker promises. See [Wiring connectors into your app](/docs/connectors/client-setup) for the full client setup. ```typescript title="src/services/rayfinClient.ts" import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental'; import { fabricSemanticModel } from '@microsoft/rayfin-connector-fabric-semanticmodel'; import type { AppSchema } from '../../rayfin/data/schema'; import type { SalesModelSchema } from '../../rayfin/connectors/salesModel/schema'; import { connectorConfig as salesModelConfig } from '../../rayfin/connectors/salesModel/schema'; type AppConnectorsSchema = { salesModel: SalesModelSchema; }; export const client = new ConnectorsRayfinClient< AppSchema, Record, AppConnectorsSchema >( { baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, connectors: { salesModel: salesModelConfig, }, }, { salesModel: fabricSemanticModel(), } ); ``` The key must match the connector `name` in `rayfin/rayfin.yml`, the `AppConnectorsSchema` property, the `connectors` option, and the runtime map. ## Call `executeQuery` [#call-executequery] `executeQuery` accepts a DAX query and resolves to an already-normalized `SemanticModelQueryResult`. Do not call `toQueryResult` on it again; branch on `result.status` directly. ```typescript title="src/features/sales/loadSalesByRegion.ts" import { client } from '../../services/rayfinClient'; export async function loadSalesByRegion() { const result = await client.connectors.salesModel.executeQuery({ query: ` EVALUATE SUMMARIZECOLUMNS( 'Sales'[Region], "Total Sales", [Total Sales] ) `, resultSetRowCountLimit: 500, }); if (result.status === 'error') { throw new Error( result.error.recoveryHint ? `${result.error.message} ${result.error.recoveryHint}` : result.error.message ); } return result.table.rows.map((row) => { const record = Object.fromEntries( result.table.columns.map((column, index) => [column.name, row[index]]) ); return { region: String(record["Sales[Region]"] ?? ''), totalSales: Number(record['[Total Sales]'] ?? 0), requestId: result.requestId, }; }); } ``` ## Render the table shape [#render-the-table-shape] A successful semantic-model result has one normalized table: ```typescript type SemanticModelQueryResult = | { status: 'success'; table: { columns: { name: string; dataType: string }[]; rows: unknown[][]; }; requestId: string; } | { status: 'error'; error: QueryError; requestId: string; }; ``` Rows are row-major arrays aligned with `columns`, so a generic renderer can use the column index instead of reading object keys from the raw Power BI response. ```typescript title="src/features/sales/SemanticModelTable.tsx" import type { SemanticModelQueryResult } from '@microsoft/rayfin-connector-fabric-semanticmodel'; export function SemanticModelTable({ result }: { result: SemanticModelQueryResult }) { if (result.status === 'error') { return (

{result.error.category}: {result.error.message}

); } return ( {result.table.columns.map((column) => ( ))} {result.table.rows.map((row, rowIndex) => ( {result.table.columns.map((column, columnIndex) => ( ))} ))}
{column.name}
{String(row[columnIndex] ?? '')}
); } ``` ## Cap rows when you need a guard [#cap-rows-when-you-need-a-guard] `ExecuteQueryInput` is: ```typescript type ExecuteQueryInput = { query: string; resultSetRowCountLimit?: number; }; ``` There is no default row limit. Omitting `resultSetRowCountLimit` returns every row the DAX query produces. Use the field when you want a guard on response size; prefer it over wrapping the DAX in `TOPN` unless you intentionally want a ranked subset. If the result exceeds the limit, the connector returns `status: 'error'` with category `'overflow'`, so the app does not mistake a truncated table for a complete answer. ## Handle error categories [#handle-error-categories] `QueryError` carries `category`, `message`, optional `code`, optional `details`, and an optional `recoveryHint`. | Category | Meaning | App response | | ---------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `network` | The request did not reach Power BI. | Retry or show a transient connectivity message. | | `api` | Power BI rejected the request, often because of auth, permissions, or throttling. | Ask the user to sign in again, wait, or check model access. | | `query` | Power BI ran the DAX and returned a query error. | Show the DAX error and let the user change the query. | | `overflow` | A row or byte cap was exceeded. | Ask for a narrower query or a higher explicit limit. | | `unknown` | The connector could not classify the failure. | Show the message and include `requestId` in diagnostics. | ## Exercise the connector from the CLI [#exercise-the-connector-from-the-cli] `connector invoke` runs a DAX payload against a registered connector: ```bash npx rayfin connector invoke salesModel executeQuery --input '{"query":"EVALUATE TOPN(10, Sales)","resultSetRowCountLimit":500}' ``` For `fabric-semanticmodel`, the command calls Fabric and Power BI directly under the developer's own identity. It works with or without `npx rayfin up`, but the connector entry must have both `workspaceId` and `itemId` under `config:`. The output is already normalized: `status: 'success'` carries `table` and `requestId`; `status: 'error'` carries `error` and `requestId`. A resolved invocation is not automatically a success, and the CLI exits non-zero for the normalized error arm. Use `connector inspect` for read-only exploration before writing app code: ```bash npx rayfin connector inspect --name salesModel npx rayfin connector inspect --name salesModel --query rayfin/queries/sales-by-region.dax ``` `connector inspect` supports semantic models, including direct selectors and portal URLs. See [CLI connector reference](/docs/reference/cli/connector) for selector and payload rules. > [!NOTE] > If `connector invoke` prints `Do not know how to serialize a BigInt` for an Int64 or > `DISTINCTCOUNT` column, the DAX query still succeeded. Select a non-Int64 column to read > the CLI output. ## Troubleshoot semantic-model queries [#troubleshoot-semantic-model-queries] | Symptom | Likely cause | Fix | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | | `rayfin up` rejects `auth.type: application` | Category B connectors are delegated-only. | Set `auth.type: delegated`. | | `executeQuery` returns a raw transport payload or cannot decode the table. | `fabricSemanticModel()` is missing from the runtime map. | Register `{ salesModel: fabricSemanticModel() }` as the client's second constructor argument. | | `client.connectors.salesModel` is not typed. | The connector key differs across `rayfin.yml`, `AppConnectorsSchema`, the `connectors` option, or the runtime map. | Use the connector `name` from `rayfin.yml` in all four places. | | A large query fails with category `overflow`. | `resultSetRowCountLimit` or a service byte cap stopped a truncated result. | Narrow the DAX query or choose a higher explicit limit. | | `connector invoke` reports `missing workspaceId/itemId in rayfin.yml`. | The semantic model connector lacks a complete `config:` block. | Re-add the connector with `--workspace-id` and `--item-id`. | | `connector invoke` exits non-zero but prints a JSON result. | The operation resolved to `status: 'error'`. | Read `error.category`, `error.message`, and `requestId`; a resolved call is not a successful query. | | The CLI prints `Do not know how to serialize a BigInt`. | The result includes an Int64 value that the CLI serializer cannot print. | Select a non-Int64 column for CLI inspection or query the same model from app code. | ```prompt title="Query a Fabric semantic model from a Rayfin app" In my Rayfin project, add a Fabric semantic model connector named salesModel and call it from the frontend. Use `npx rayfin connector add --type fabric-semanticmodel --workspace-id --item-id --name salesModel`, run the pinned npm install command the CLI prints, and do not edit rayfin/connectors/salesModel/schema.ts by hand. Wire `SalesModelSchema` into `AppConnectorsSchema`, pass `connectorConfig` in the ConnectorsRayfinClient `connectors` option, and register `{ salesModel: fabricSemanticModel() }` in the runtime map. Then call `client.connectors.salesModel.executeQuery({ query, resultSetRowCountLimit: 500 })`, branch on `result.status`, render `result.table.columns` with row-major `result.table.rows`, and show `result.error.category`, `result.error.message`, and `result.requestId` on failure. ``` --- --- title: "Fabric SQL sources" description: "Read and write Fabric SQL connector entities from Rayfin apps, including query chains, by-key reads, mutations, defaults, and troubleshooting." url: https://rayfin.ai/docs/connectors/sql-sources markdown_url: https://rayfin.ai/docs/connectors/sql-sources.md section: connectors product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: connectors/sql-sources.mdx --- # Fabric SQL sources > Read and write Fabric SQL connector entities from Rayfin apps, including query chains, by-key reads, mutations, defaults, and troubleshooting. Use a Category A connector when an existing Fabric SQL source should feel like typed Rayfin entities. Category A covers `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase`; see [Connectors](/docs/connectors) for the full connector-type catalog and the steps that create the connector entry. Each generated entity is reached at `client.connectors..`. The methods on that entity come from two gates: * The connector's `operations:` list in `rayfin.yml` decides which CRUD verbs exist. * The connector type decides the write dialect, including whether mutations return the row or a status object. ## Choose the expected shape [#choose-the-expected-shape] * **Lakehouse SQL analytics endpoint (`fabric-sqlanalytics`)** — reads only. Write methods are not exposed. * **Fabric SQL Database (`fabric-sqldatabase`)** — full CRUD when `operations:` allows it; writes return the full persisted row. * **Fabric Warehouse (`fabric-warehouse`)** — full CRUD when `operations:` allows it; writes return `DbOperationResult { result: string }`. ## Read rows with the query chain [#read-rows-with-the-query-chain] Reads work the same way on Lakehouse, Warehouse, and SQL Database connectors. Start from an entity, choose a selection, add filters or ordering, and call `execute()`. ```typescript const orders = await client.connectors.inventory.Order .select(['orderId', 'customerEmail', 'total']) .where({ total: { gt: 100 } }) .orderBy({ total: 'desc' }) .execute(); const firstPage = await client.connectors.inventory.Order .select(['orderId', 'customerEmail', 'total']) .where({ customerEmail: { contains: '@contoso.com' } }) .orderBy({ orderId: 'asc' }) .first(25) .execute(); const savedCursor = 'opaque-page-cursor-from-the-previous-response'; const nextPage = await client.connectors.inventory.Order .select(['orderId', 'customerEmail', 'total']) .orderBy({ orderId: 'asc' }) .first(25) .after(savedCursor) .execute(); ``` `select`, `where`, `orderBy`, `first`, and `after` are read operations. They are available when the connector includes `read` in `operations:`. ## Read one row by key [#read-one-row-by-key] `findByKey` identifies a row with a key object. For a composite primary key, the object must include every key part; omitting one is a compile error. Pass the required scalar-only `select` when projecting a by-key read. Relationships cannot be selected through `findByKey`; use the query chain for related data. ```typescript const order = await client.connectors.inventory.Order.findByKey( { orderId: 'o-1' }, ['orderId', 'customerEmail', 'total'], ); const lineItem = await client.connectors.inventory.OrderItem.findByKey( { orderId: 'o-1', productId: 'p-9' }, ['orderId', 'productId', 'quantity'], ); lineItem?.quantity; ``` The projected result is `Pick | null`. A keyless entity exposes no `findByKey` method. ## Read related columns with dotted paths [#read-related-columns-with-dotted-paths] `select` accepts scalar columns and dotted paths through generated `@one` and `@many` navigation fields. The builder expands each dotted path into the nested GraphQL selection and unwraps to-many `items` connections so related rows are inline in the response. ```typescript const products = await client.connectors.inventory.Product .select(['name', 'category.name', 'orderItems.quantity']) .where({ stock: { gt: 0 } }) .execute(); products[0].category.name; products[0].orderItems[0].quantity; ``` Paths can continue through more relationships. ```typescript const products = await client.connectors.inventory.Product .select(['name', 'orderItems.order.customerEmail']) .execute(); ``` Name a related column as a dotted path such as `category.name`. Naming the bare relationship, such as `category`, is a compile error because a navigation field is not a selectable leaf. Each segment is checked against the generated schema. Self-referencing foreign keys still generate an entity relationship for DAB, but the client cannot query across a self-relationship. Do not select a dotted path over a self-reference. ## Write rows by dialect [#write-rows-by-dialect] Write methods exist only when the connector's `operations:` includes the verb and the entity's `@role()` grants it. Autocomplete is expected to omit a method that is outside the connector operation list. | Connector type | Source behavior | `create` / `update` / `delete` return | What to do | | ----------------------------------- | ---------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `fabric-sqlanalytics` (Lakehouse) | Read-only at the host | Method does not exist | Treat missing write methods as expected compile-time protection. | | `fabric-sqldatabase` (SQL Database) | Supports read-after-write | The full entity row | Use the returned row, including server-generated columns the caller never sent. Requires the `entities` map. | | `fabric-warehouse` (Warehouse) | DWSQL has no `OUTPUT` clause | `DbOperationResult { result: string }` | Treat `"success"` as completion; failed writes throw GraphQL errors. Re-query if you need persisted values. | ```typescript const created = await client.connectors.orders.Order.create({ customerEmail: 'ada@example.com', total: 250, }); created.orderId; created.createdUtc; const result = await client.connectors.inventory.Order.update( { orderId: 'o-1' }, { total: 275 }, ); result.result; await client.connectors.inventory.OrderItem.delete({ orderId: 'o-1', productId: 'p-9', }); ``` `update` and `delete` take the same full key object as `findByKey`. A keyless entity, where `Source({ primaryKey })` is omitted or `primaryKey: []`, exposes no `findByKey`, `update`, or `delete` at all. It is read-only by key regardless of connector type. ## Handle server-generated columns [#handle-server-generated-columns] A generated entity marks server-filled columns as `AutoGenerated`. The metadata that triggers this type wrapper is: * `IDENTITY` columns. * Any column with a `DEFAULT` expression. * Computed columns declared with `AS (...)`. * Server-managed rowversion or temporal-period columns. Those columns are optional in `create` and `update` inputs and read back as plain `T` in query and mutation results. Most cannot be written: passing an `IDENTITY`, computed, rowversion, or temporal value is rejected by the database. A plain `DEFAULT` column is the exception: omit it to get the default, or pass a value to override it. ```typescript title="rayfin/connectors/orders/Order.ts" import { entity, date, decimal, int, Source } from '@microsoft/rayfin-core/experimental'; import type { AutoGenerated } from '@microsoft/rayfin-core/experimental'; @entity() export class Order extends Source({ schema: 'dbo', table: 'Order', primaryKey: ['orderId'] }) { @int({ column: 'OrderID' }) orderId!: AutoGenerated; @decimal({ precision: 18, scale: 2 }) total!: number; @date({ column: 'CreatedUtc' }) createdUtc!: AutoGenerated; } ``` ## Configure default selections with `entities` [#configure-default-selections-with-entities] `connectorConfig.entities` gives the runtime the scalar property names for each entity. It is what makes no-selection reads work, and it is how `fabric-sqldatabase` writes know which columns to read back after a mutation. ```typescript title="rayfin/connectors/inventory/schema.ts" import type { GraphQLBackedConnector } from '@microsoft/rayfin-connector-fabric-graphql'; import type { ConnectorConfig } from '@microsoft/rayfin-connectors'; import type { Order } from './Order.js'; import type { OrderItem } from './OrderItem.js'; export type { Order } from './Order.js'; export type { OrderItem } from './OrderItem.js'; export const connectorConfig = { connector: 'fabric-warehouse', operations: ['read', 'create', 'update', 'delete'], entities: { Order: ['orderId', 'customerEmail', 'total', 'createdUtc'], OrderItem: ['orderId', 'productId', 'quantity', 'unitPrice'], }, } as const satisfies ConnectorConfig; export type InventorySchema = GraphQLBackedConnector< { Order: typeof Order; OrderItem: typeof OrderItem }, typeof connectorConfig >; ``` Without `entities`, every no-selection `findMany`, `findFirst`, or `findByKey` throws `SELECTION_REQUIRED` on every dialect. Passing an explicit `select([...])` remains the most precise way to control a read projection. Use entity property names, not database column names. If the entity declares `@uuid({ column: 'ProductID' }) productId!: string`, the map entry is `productId`. The one exception is a field that declares `graphqlName`; then use that GraphQL field name. Leave relationship fields out of the string-array form. A relationship `select` against the string-array form throws `ENTITIES_REQUIRED_FOR_RELATIONSHIP_SELECT`, because arrays of field names do not carry the `@one` and `@many` cardinality metadata needed to shape nested GraphQL. Select the foreign key scalar and fetch the related entity separately, or run relationship reads in code that can provide the decorated classes. ## Aggregate connector rows [#aggregate-connector-rows] Aggregations use the same client pattern as Rayfin data entities. Use `groupBy(fields).aggregate(spec).execute()` for grouped values, or call `aggregate(spec)` on the entity client for a grand total. See [Aggregations](/docs/data/aggregations) for the full API and response shape. ```typescript const totalsByCustomer = await client.connectors.inventory.Order .groupBy(['customerEmail']) .aggregate({ total: { sum: true, avg: true, count: true } }) .execute(); const grandTotal = await client.connectors.inventory.Order .aggregate({ total: { sum: true, min: true, max: true } }) .execute(); ``` All five aggregate operations, `sum`, `avg`, `min`, `max`, and `count`, accept numeric fields only on connector entities. ## Troubleshoot connector entity calls [#troubleshoot-connector-entity-calls] | Symptom | Likely cause | Fix | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | A read throws `SELECTION_REQUIRED` | `connectorConfig.entities` is missing and the call did not pass an explicit selection | Add the entity's scalar property names to `entities`, or pass `.select([...])` / a selected read. | | A relationship read throws `ENTITIES_REQUIRED_FOR_RELATIONSHIP_SELECT` | The connector registered `entities` as string arrays, so runtime relationship cardinality is unavailable | Select foreign key scalars and fetch separately, or run the relationship read where decorated classes can be registered. | | A CRUD method is missing from autocomplete | Expected: the typed marker narrows methods to the connector's `operations:` and removes by-key methods from keyless entities | Check `rayfin.yml`, `connectorConfig.operations`, and `Source({ primaryKey })`; widen only if the source and policy should allow it. | ```prompt title="Query and update a Fabric SQL connector" In my Rayfin project, use the existing Category A connector named inventory. Inspect rayfin/connectors/inventory/schema.ts and the generated entity files to confirm the connector type, operations, primary keys, scalar fields, and relationship fields. Then add a client-side query that reads Order rows through client.connectors.inventory.Order with an explicit select, a where filter, and an orderBy. If the connector exposes update, update one Order by passing the full key object and the scalar fields to change. If the connector is a Warehouse, treat the mutation result as DbOperationResult and re-query the row with findByKey if persisted values are needed; if it is a SQL Database, use the returned row. ``` --- --- title: "Environments and configuration" description: "Every environment variable file, prefix, and resolution rule Rayfin tooling reads, from frontend-visible variables to interpolation in rayfin.yml." url: https://rayfin.ai/docs/deploy/environments markdown_url: https://rayfin.ai/docs/deploy/environments.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: deploy/environments.mdx --- # Environments and configuration > Every environment variable file, prefix, and resolution rule Rayfin tooling reads, from frontend-visible variables to interpolation in rayfin.yml. Rayfin tooling reads and writes configuration through a small set of files and a handful of variable prefixes. This page covers the concepts you need while deploying — where each kind of value lives, what it's for, and how `rayfin.yml` pulls values in from the environment. For the complete, exhaustive variable-by-variable table, see [Environment variables](/docs/reference/config/environment-variables). ## File locations [#file-locations] | Path | Purpose | Committed | | ---------------------------- | ---------------------------------------------------------------------- | --------------- | | `rayfin/.env` | All runtime and deployment values. | No (gitignored) | | `rayfin/.env.example` | Documents expected variables with placeholder values. | Yes | | `rayfin/.deployments.json` | Multi-deployment registry (item IDs, API URLs, workspace IDs). | No (gitignored) | | `rayfin/rayfin.yml` | Project configuration, service toggles, frontend framework. | Yes | | `.env.local` | Framework-specific frontend variables, auto-generated by `rayfin env`. | No (gitignored) | | `~/.rayfin/auth-state.json` | CLI authentication state (tenant, account hints). | N/A (user home) | | `~/.rayfin/token-cache.json` | Encrypted token cache (OS-backed encryption). | N/A (user home) | ## Frontend-visible variables (RAYFIN\_PUBLIC\_\*) [#frontend-visible-variables-rayfin_public_] Variables prefixed `RAYFIN_PUBLIC_` live in `rayfin/.env` and are the **only** variables exposed to frontend builds. `rayfin up` populates most of these after a deploy; `rayfin env` (run automatically by the scaffolded `predev`/`prebuild` scripts) maps them into a framework-specific `.env.local`. | Variable | Description | Populated by | | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------ | | `RAYFIN_PUBLIC_API_URL` | Rayfin backend URL, generated after `rayfin up` deploys to a Fabric app. | `rayfin up` | | `RAYFIN_PUBLIC_PUBLISHABLE_KEY` | Public key for Rayfin SDK initialization. | `rayfin up` | | `RAYFIN_PUBLIC_ITEM_ID` | Fabric app item ID. Used for Fabric brokered auth. | `rayfin up` | | `RAYFIN_PUBLIC_WORKSPACE_ID` | Fabric workspace ID. Used for Fabric brokered auth. | `rayfin up` | | `RAYFIN_PUBLIC_TENANT_ID` | Entra ID tenant for workspace disambiguation. | `rayfin up` | | `RAYFIN_PUBLIC_PORTAL_URL` | Fabric portal base URL. | `rayfin up` | | `RAYFIN_PUBLIC_SERVICE_MODE` | `rayfin` (real backend) or `mock` (local testing). | User-set | | `RAYFIN_PUBLIC_FRONTEND_PORT` | Stable per-project frontend dev-server port, so the deployed backend can allow-list a deterministic origin. | `rayfin up` | For Vite, `RAYFIN_PUBLIC_API_URL` becomes `VITE_RAYFIN_API_URL`, `RAYFIN_PUBLIC_PUBLISHABLE_KEY` becomes `VITE_RAYFIN_PUBLISHABLE_KEY`, and so on — a custom `RAYFIN_PUBLIC_FOO` becomes `VITE_RAYFIN_FOO` (Next.js: `NEXT_PUBLIC_RAYFIN_FOO`; plain: `FOO`). See [Environment variables](/docs/reference/config/environment-variables) for the full per-framework mapping table, and [`rayfin env`](/docs/reference/cli/env) for the command that generates `.env.local`. ## Tooling overrides [#tooling-overrides] These configure CLI behavior and are never exposed to the frontend. Set them in `rayfin/.env` or as shell variables. | Variable | Description | Default | | -------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------- | | `RAYFIN_FABRIC_API_URL` | Fabric REST API base URL the CLI calls. Useful for routing through a credential proxy. | `https://api.fabric.microsoft.com/v1` | | `RAYFIN_FABRIC_PORTAL_URL` | Fabric portal base URL used for deep links and `RAYFIN_PUBLIC_PORTAL_URL`. | `https://app.fabric.microsoft.com/` | | `RAYFIN_ENV_FILE` | Path to an alternate `.env` file. Equivalent to `--env-file`. | `rayfin/.env` | ## Service configuration flags [#service-configuration-flags] Written to `rayfin/.env` based on your `rayfin.yml` settings, and read by the Rayfin WebService: | Variable | Source (`rayfin.yml`) | Values | | ------------------ | -------------------------- | ---------------- | | `Auth__Enabled` | `services.auth.enabled` | `true` / `false` | | `Data__Enabled` | `services.data.enabled` | `true` / `false` | | `Storage__Enabled` | `services.storage.enabled` | `true` / `false` | ## Shell-only variables [#shell-only-variables] Read from the shell environment only — never written to a file. These are the ones you'll use most often around deployment and CI/CD: | Variable | Description | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RAYFIN_TOKEN` | Pre-acquired token for headless or non-interactive usage, bypassing interactive login. Prefer `rayfin login --service-principal` unless a token is already available from an external source. | | `RAYFIN_TENANT_ID` | Entra ID tenant used by `rayfin up`. Equivalent to `-t, --tenant ` (precedence: flag > env var > signed-in tenant). | | `RAYFIN_ENCRYPTION_FALLBACK_ENABLED` | Set to `true` to allow plaintext token cache on systems without OS credential storage. Development only. | | `RAYFIN_WORKSPACE_ID` | Fabric workspace ID for non-interactive setup, used with `RAYFIN_TOKEN`. | | `RAYFIN_FEATURE_FLAGS` | Comma-separated experimental feature names to enable. | | `RAYFIN_APPINSIGHTS_CONNECTION_STRING` | Override the telemetry endpoint for the CLI and VS Code extension. | ## Resolution priority [#resolution-priority] When the same variable is set in more than one place, Rayfin resolves it in this order (highest priority first): 1. Shell environment variable. 2. `--env-file ` CLI flag (or `RAYFIN_ENV_FILE`). 3. `rayfin/.env` file. 4. Default value (hardcoded, or from `rayfin.yml` interpolation). ## Variable interpolation in rayfin.yml [#variable-interpolation-in-rayfinyml] `rayfin.yml` supports shell-style variable interpolation, so you can keep environment-specific values (connection strings, API keys, URLs) out of the checked-in config. ```yaml title="rayfin/rayfin.yml" services: data: host: ${DB_HOST} port: ${DB_PORT:-1433} ``` * `${VAR}` — substitutes the variable. Fails with a clear error if it is unset or empty. * `${VAR:-default}` — substitutes the variable, or `default` if it is unset **or** empty (an empty string counts as unset). ```bash title="rayfin/.env" DEFINED=value EMPTY= # UNDEFINED is not set ``` ```yaml config1: ${DEFINED} # → "value" config2: ${EMPTY:-fallback} # → "fallback" (empty, uses default) config3: ${UNDEFINED:-fallback} # → "fallback" (unset, uses default) config5: ${EMPTY} # → Error! (empty without default) config6: ${UNDEFINED} # → Error! (unset without default) ``` When a value is **entirely** a variable reference, Rayfin coerces it to the matching YAML type — `port: ${DB_PORT}` with `DB_PORT=1433` in `.env` becomes the number `1433`, not the string `"1433"`. Partial interpolation (`http://localhost:${PORT}`) always produces a string. `.env` values are resolved with the same [priority](#resolution-priority) as everything else: shell environment first, then the `.env` file, then the `:-` default. See [Environment variable interpolation](/docs/reference/config/env-interpolation) for the full syntax reference, including error handling details. ```prompt title="Move a hard-coded value in rayfin.yml into .env" In my Rayfin project's rayfin/rayfin.yml, replace the hard-coded connection string under services.data with a ${DB_CONNECTION_STRING} interpolation. Add DB_CONNECTION_STRING to rayfin/.env with the current value, and add rayfin/.env to .gitignore if it isn't already there. Confirm rayfin.yml still resolves correctly by running `npx rayfin up -n` (dry run). ``` --- --- title: "Fabric apps" description: "What a managed Fabric app is, its prerequisites and child services, and how to create and manage one from the Microsoft Fabric portal." url: https://rayfin.ai/docs/deploy/fabric-apps markdown_url: https://rayfin.ai/docs/deploy/fabric-apps.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:46:21-07:00 source: deploy/fabric-apps.mdx --- # Fabric apps > What a managed Fabric app is, its prerequisites and child services, and how to create and manage one from the Microsoft Fabric portal. A **Fabric app** is a Fabric item that hosts your Rayfin project as a managed service. Fabric provisions and operates the database, authentication, static hosting, and API endpoints, so you maintain application code instead of infrastructure. ## What a Fabric app contains [#what-a-fabric-app-contains] Every Fabric app exposes a single Rayfin endpoint backed by a set of child services: ```mermaid flowchart TD App["Fabric app"] --> Auth["Auth"] App ==> Static["Static content"] App ==> Web["WebService"] Web ==> DataApi["Data API Builder"] Web ==> Fn["Functions"] Web ==> Blob[("Blob storage")] DataApi ==> MSSQL[("MSSQL")] Fn ==> MSSQL Auth -.->|"Fabric SSO"| Entra["Microsoft Entra ID"] class App,Auth,Static,Web,DataApi service class MSSQL store class Fn,Blob experimental class Entra external ``` Each node maps to a key under `services` in [`rayfin.yml`](/docs/reference/config/rayfin-yml): | Service | `rayfin.yml` key | What it provides | | ---------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Auth | `services.auth` | Session issuing and validation. Sign-in itself is delegated to Microsoft Entra ID through [Fabric SSO](/docs/auth/fabric-sso) — the identity provider lives outside the app. | | Data API Builder | `services.data` | The [GraphQL data API](/docs/data) over the app's MSSQL database. `dialect` is `mssql`. | | Static content | `services.staticHosting` | Your built frontend, served from the app's origin. See [Static hosting](/docs/hosting). | | Functions | `services.functions` | Server-side [TypeScript functions](/docs/functions). Experimental. | | Blob storage | `services.storage` | [File and blob storage](/docs/storage). Experimental. | Auth, data, and static hosting are the core services. Functions and blob storage are optional — enable them in `rayfin.yml` when you need them. > [!WARNING] > Functions and blob storage are experimental and are not available in every Fabric region > or tenant. Confirm availability in your tenant before you depend on them. A Fabric app lives inside a Fabric workspace. A workspace can hold multiple Fabric apps — for example, one per environment or one per project. ## Prerequisites [#prerequisites] ### Fabric capacity [#fabric-capacity] The workspace that will hold your Fabric app must have Fabric capacity assigned — select a capacity when you create the workspace if it does not already have one. Every service your app uses consumes capacity units from that assignment. See [Capacity and billing](/docs/deploy/pricing) for what consumes capacity and what does not. ### Tenant admin setting [#tenant-admin-setting] A Fabric tenant administrator must enable the Fabric app workload before anyone in the tenant can create one: 1. Sign in to the [Fabric admin portal](https://app.fabric.microsoft.com/admin-portal). 2. Go to **Tenant settings**. 3. Under **Fabric Apps (preview)**, toggle the setting to **Enabled**. 4. Choose whether to enable it for the whole organization or specific security groups. 5. Click **Apply**. Changes can take a few minutes to propagate. If you are not a tenant admin, ask your Fabric administrator to complete this step before trying to create a Fabric app. ## Create a Fabric app in the portal [#create-a-fabric-app-in-the-portal] 1. Open [Microsoft Fabric](https://app.fabric.microsoft.com) and sign in with your Microsoft account. 2. Select a workspace from the left navigation, or create one: **Workspaces** → **New workspace** → enter a name and select a Fabric capacity. 3. In the workspace, click **New item**, then search for and select **App (preview)** — this is the item type a Rayfin project deploys into. 4. Enter a name (for example, `my-rayfin-app`) and click **Create**. 5. Click **Open in VS Code** on the new item to load the project, then use GitHub Copilot to build your app. 6. When you are ready to ship, run `npx rayfin up` from the project's terminal. See [Deploying with rayfin up](/docs/deploy/rayfin-up) for the full workflow. ```prompt title="Create and deploy a Fabric app" I have a Rayfin project ready to ship. Sign me in with `npx rayfin login`, then run `npx rayfin up` to create a Fabric app for it (or update the existing one, if rayfin/.deployments.json already has a deployment) and deploy the current build. Once it finishes, run `npx rayfin up status` and tell me the live hosting URL. ``` ## Child services [#child-services] `rayfin up` provisions these as child items under the Fabric app, based on your `rayfin.yml`: | Child service | What it provides | Portal capabilities | | ------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **SQL Database** | An MSSQL database with the schema generated from your TypeScript data model decorators. | View the database and run queries with the query editor, or copy the connection string. Read-only — schema changes must come from your code via `rayfin up`. | | **Authentication** | Fabric brokered auth using Microsoft Entra ID (SSO). Users sign in with their existing Fabric identity. | View authenticated users in the SQL Database. | | **Static Content** | Your built frontend assets (HTML, CSS, JS), served at a public URL from OneLake storage. | View the hosting URL. Assets update on every deploy. | ## The Rayfin endpoint [#the-rayfin-endpoint] Every Fabric app has one Rayfin endpoint that fronts all of its services: ```text https://-app.rayfin.windows.net/ ``` | Path | Service | | -------------- | ---------------------------------------------------------------- | | `/api/graphql` | Data API (GraphQL) — used by `RayfinClient` for CRUD operations. | | `/auth` | Authentication service. | | `/storage` | File storage. | Your frontend reads this endpoint from the `RAYFIN_PUBLIC_API_URL` variable in `rayfin/.env`. See [Environments and configuration](/docs/deploy/environments) for how that value is generated into `.env.local` for your framework. ## Manage it in the Fabric portal [#manage-it-in-the-fabric-portal] Open the Fabric app in the portal to see its **Rayfin endpoint**, its **App URL** (the public static content URL), and a link back to the Fabric portal. Click into it to see child items: the **SQL Database** (opens the query editor for read-only queries) and **Authentication** (view signed-in users). Schema changes made directly in the portal's query editor are overwritten on the next `rayfin up`. ### Permissions [#permissions] Workspace roles do not automatically carry item-level permissions. To let someone in your organization open and use the app, grant them **Run and interact** on the Fabric app item. | Permission | What it allows | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Run and interact** (default) | Open and use the deployed app. Every workspace member gets this by default. | | **Edit (Write)** | Deploy code with `rayfin up`, apply schema changes, update settings, and manage child services. Requires **contributor** or **admin** on the workspace. | | **Reshare** | Grant other users access to the Fabric app. Requires **admin** on the workspace. | See [Workspace roles](https://learn.microsoft.com/fabric/fundamentals/roles-workspaces) in the Microsoft Fabric documentation for how workspace roles work. ## Next steps [#next-steps] * [Deploying with rayfin up](/docs/deploy/rayfin-up) — the full deploy workflow and CLI flags. * [Capacity and billing](/docs/deploy/pricing) — what running this app costs in Fabric capacity units. --- --- title: "Deploy" description: "Deploy a Rayfin project to a managed Fabric app with the rayfin up CLI workflow, then manage secrets, environments, and billing." url: https://rayfin.ai/docs/deploy markdown_url: https://rayfin.ai/docs/deploy.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: deploy/index.mdx --- # Deploy > Deploy a Rayfin project to a managed Fabric app with the rayfin up CLI workflow, then manage secrets, environments, and billing. Deploying ships your Rayfin project to a **Fabric app** — a managed, Microsoft Fabric-hosted instance of your backend and frontend. Fabric provisions the database, authentication, static hosting, and API endpoints; one CLI command builds and ships all of it. ```bash npx rayfin login npx rayfin up ``` `rayfin up` builds your static content, deploys it, pushes your `rayfin.yml` settings to the remote service, and applies any pending database schema changes — all in one step. Run it again for every change you want to ship, including schema-only changes. Confirm the deployment is healthy: ```bash npx rayfin up status ``` ## In this section [#in-this-section] * **[Fabric apps](/docs/deploy/fabric-apps)** — what a managed Fabric app is, its prerequisites, and how to create one in the Fabric portal. * **[Deploying with rayfin up](/docs/deploy/rayfin-up)** — the full deploy workflow, useful flags, and what gets written to `rayfin/.deployments.json`. * **[Secrets](/docs/deploy/secrets)** — store and apply secret values your deployed app needs at runtime. * **[Environments and configuration](/docs/deploy/environments)** — every environment variable Rayfin tooling reads and writes, and where each one lives. * **[Capacity and billing](/docs/deploy/pricing)** — how Fabric capacity billing works for a deployed app. * **[Deployment troubleshooting](/docs/deploy/troubleshooting)** — symptom-to-fix reference for the most common deploy failures. --- --- title: "Capacity and billing" description: "How Fabric capacity billing works for a deployed Rayfin app, and which operations consume Capacity Units and which do not." url: https://rayfin.ai/docs/deploy/pricing markdown_url: https://rayfin.ai/docs/deploy/pricing.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: deploy/pricing.mdx --- # Capacity and billing > How Fabric capacity billing works for a deployed Rayfin app, and which operations consume Capacity Units and which do not. A Fabric app runs on Microsoft Fabric capacity. There are no additional Rayfin-specific charges — you pay only for the Fabric capacity units (CUs) consumed by the services your app uses. ## How billing works [#how-billing-works] Fabric uses a universal billing model based on **Capacity Units (CUs)**. Every operation performed by a Fabric app's child services consumes CUs from the Fabric capacity assigned to your workspace. Your workspace must have a Fabric capacity associated with it. CU consumption is tracked in the [Microsoft Fabric Capacity Metrics app](https://learn.microsoft.com/fabric/enterprise/metrics-app), where you can monitor usage per item and per operation. ## What consumes capacity [#what-consumes-capacity] A Fabric app uses three Fabric services that consume CUs. ### SQL Database [#sql-database] | Operation | What it covers | Billing meter | Type | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------- | | **SQL Usage** | Compute for all SQL queries, modifications, and data processing — including queries from your app's GraphQL API and any queries you run in the Fabric portal query editor. | SQL database in Microsoft Fabric Capacity Usage CU | Interactive | | **Allocated SQL Storage** | Dynamically allocated storage for tables, indexes, transaction logs, and metadata. Fully integrated with OneLake. | SQL Storage Data Stored | Background | One Fabric CU equals 0.383 SQL database vCores. ### GraphQL API [#graphql-api] Every GraphQL query (read) and mutation (write) your app's `RayfinClient` makes consumes CUs, at a rate of ten CUs per hour of request and response processing time. | Operation | What it covers | Billing meter | Type | | --------- | --------------------------------------------------------------------------------- | --------------------------------------- | ----------- | | **Query** | Compute for all GraphQL queries and mutations performed against your data models. | API for GraphQL Query Capacity Usage CU | Interactive | See [Fabric API for GraphQL](https://learn.microsoft.com/en-us/fabric/enterprise/fabric-operations#fabric-api-for-graphql) in the Fabric operations documentation for more detail. ### OneLake storage (static content) [#onelake-storage-static-content] When static hosting is enabled, your built frontend assets (HTML, CSS, JS) are stored in OneLake and served from a public URL. OneLake storage and the read/write operations that serve that content consume CUs. | Operation | What it covers | Billing meter | Type | | ------------------- | --------------------------------------------------------------------------- | ------------------------------------------ | ---------- | | **OneLake Read** | Read operations when serving static content to end users. | OneLake Read Operations Capacity Usage CU | Background | | **OneLake Write** | Write operations when deploying or updating static content via `rayfin up`. | OneLake Write Operations Capacity Usage CU | Background | | **OneLake Storage** | Storage of static content files in OneLake. | OneLake Storage | Background | ## What does not consume additional capacity [#what-does-not-consume-additional-capacity] These Fabric app capabilities do not incur separate CU charges today: * **Rayfin WebService** — the application backend service that handles API routing and authentication. * **Authentication** — Fabric brokered auth (Entra SSO) sign-in and session management. * **Deployment operations** — running `rayfin up` has no CU charge of its own, beyond the SQL and OneLake operations it triggers. ## Further reading [#further-reading] * [Fabric operations](https://learn.microsoft.com/en-us/fabric/enterprise/fabric-operations) — the full list of Fabric operations and their capacity consumption rates. * [Microsoft Fabric Capacity Metrics app](https://learn.microsoft.com/en-us/fabric/enterprise/metrics-app) — monitor and understand your capacity usage. * [Fabric apps](/docs/deploy/fabric-apps) — what a Fabric app is and the services that make it up. --- --- title: "Deploying with rayfin up" description: "Deploy a Rayfin project to Microsoft Fabric with rayfin up, covering login, useful flags, deployment metadata, and redeploys." url: https://rayfin.ai/docs/deploy/rayfin-up markdown_url: https://rayfin.ai/docs/deploy/rayfin-up.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: deploy/rayfin-up.mdx --- # Deploying with rayfin up > Deploy a Rayfin project to Microsoft Fabric with rayfin up, covering login, useful flags, deployment metadata, and redeploys. `rayfin up` is the canonical command for shipping a Rayfin project to a Fabric app. It builds your static content, deploys it, syncs your `rayfin.yml` settings, and applies any pending database schema changes — all in one step. Use it for the first deploy and for every change after that, including schema-only changes. > [!NOTE] > This page walks through the deploy workflow end to end. For the exhaustive flag and > subcommand reference, see [`up`](/docs/reference/cli/up) and > [`login`](/docs/reference/cli/login) in the CLI reference. ## Prerequisites [#prerequisites] * A Rayfin project with a `rayfin/rayfin.yml` configuration file. * A Microsoft account with access to a Fabric workspace. ## Sign in [#sign-in] Authenticate with your Microsoft Entra ID account before deploying: ```bash npx rayfin login ``` The CLI opens a browser window for interactive sign-in — the MSAL account picker is always shown, so you can pick a different signed-in account without passing any extra flag. After authentication, tokens are stored securely in the OS keychain under `~/.rayfin/`. Check your sign-in status at any time: ```bash npx rayfin login status ``` See [`rayfin login`](/docs/reference/cli/login) for the full set of login flags. ### Non-interactive login [#non-interactive-login] Authenticate as a service principal using client credentials when interactive browser login is not available or wanted — for example, in a CI/CD pipeline: ```bash npx rayfin login --service-principal \ --client-id \ --client-secret \ --tenant ``` Credentials persist to `~/.rayfin/`, so every subsequent command in the same pipeline job authenticates automatically without a browser or user interaction. Alternatively, set the `RAYFIN_TOKEN` shell environment variable to a pre-acquired token to bypass interactive login entirely. See [Environments and configuration](/docs/deploy/environments) for the full set of shell-only variables. ## Deploy with rayfin up [#deploy-with-rayfin-up] Run this from your project root: ```bash npx rayfin up ``` If you are not signed in, the CLI launches the interactive login flow automatically. ### What rayfin up does [#what-rayfin-up-does] 1. **Creates a Fabric app** in your workspace on the first deploy, or reuses the existing one on subsequent deploys. 2. **Retrieves the publishable key** from the remote service. 3. **Syncs runtime settings** from `rayfin.yml` to the remote service, including auth configuration and which services are enabled. 4. **Applies the database schema** generated from your TypeScript data model decorators. 5. **Builds and deploys static content**, if `staticHosting` is enabled — runs your build command, packages the output, and uploads it. 6. **Persists deployment details** to `rayfin/.deployments.json` and merges the matching `RAYFIN_PUBLIC_*` values into `rayfin/.env`. After it finishes, the CLI prints the **hosting URL** where your app is live, a **Fabric portal link** to manage the deployment, and the **deployment ID**. > [!NOTE] > Fabric brokered authentication (Entra SSO) is the only supported sign-in method. Make > sure `services.auth.fabric.enabled` is `true` in `rayfin.yml` before deploying if your > app needs sign-in. See [Fabric SSO](/docs/auth/fabric-sso). ### Useful flags [#useful-flags] | Flag | What it does | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-t, --tenant ` | Target a specific Entra ID tenant, when your account spans multiple tenants. | | `-w, --workspace ` | Target a specific Fabric workspace by display name. Defaults to "My Workspace" when omitted. | | `--workspace-id ` | Target a specific Fabric workspace by GUID instead of by display name. | | `--workspace-uri ` | Target a workspace by its Fabric portal URL — the CLI derives the workspace ID and target environment from it. | | `--force` | Allow destructive schema changes (dropping a column or table, for example) that may cause data loss. | | `-n, --dry-run` | Preview what the CLI would do without creating or modifying any resources. | | `--env-file ` | Path to the `.env` file to read. Defaults to `rayfin/.env`. | | `-v, --verbose` | Print detailed output, useful when diagnosing a failed deploy. | | `--json` | Print the deployment result as JSON, for scripting or automation. | | `-y, --yes` | Auto-accept all confirmation prompts — for non-interactive use. | | `--exclude-services ` | Comma-separated services to skip. Only `staticHosting` is currently supported — runtime settings are still synced, so the backend is never silently out of date. The scaffolded `npm run dev` script uses this so a local Vite dev server can serve the frontend while the backend stays deployed. | | `--encryption-fallback-enabled` | Allow plaintext token storage on systems without OS credential storage — some Linux distros, dev containers, and Codespaces. Only pass this when login fails with a keychain error. | `-w/--workspace`, `--workspace-id`, and `--workspace-uri` are three ways to target the same thing — pass at most one. ```bash npx rayfin up -n -v npx rayfin up --workspace-id 8b17cf64-3c12-46ac-a572-192732c32641 npx rayfin up --exclude-services staticHosting ``` ```prompt title="Build and deploy my app" Build and deploy my Rayfin project to Microsoft Fabric. Sign me in if needed with `npx rayfin login`, then run `npx rayfin up` to build the static app, sync settings, and apply any pending schema changes. Once it finishes, run `npx rayfin up status` to confirm the deployment is healthy and tell me the live hosting URL. ``` ## Subsequent deployments [#subsequent-deployments] After the first deploy, `rayfin/.deployments.json` records the deployment and the matching `RAYFIN_PUBLIC_*` values are merged into `rayfin/.env`. Running `npx rayfin up` again updates that same deployment rather than creating a new one. For targeted updates, use the subcommands instead of a full redeploy: | Command | What it updates | | -------------------------------- | ---------------------------------------------------- | | `npx rayfin up` | Everything — settings, database, and static content. | | `npx rayfin up db apply` | Database schema only. | | `npx rayfin up staticapp deploy` | Static content only. | ### Apply database changes remotely [#apply-database-changes-remotely] After changing an entity under `rayfin/data/`, push the schema change to the remote database without redeploying the full stack: ```bash npx rayfin up db apply ``` If the change could be destructive (dropping a column or table, for example), the CLI warns you and refuses to proceed. Use `--force` only after confirming you accept the data loss: ```bash npx rayfin up db apply --force ``` ### Redeploy static content [#redeploy-static-content] When you have only changed frontend code, redeploy static content on its own for a faster iteration cycle: ```bash npx rayfin up staticapp deploy ``` This runs your configured `buildCommand`, packages the output, and uploads it. To skip the build step and deploy existing output: ```bash npx rayfin up staticapp deploy --skip-build ``` ## Check deployment status [#check-deployment-status] ```bash npx rayfin up status ``` Add `--json` for machine-readable output: ```bash npx rayfin up status --json ``` ## Deployment metadata: rayfin/.deployments.json [#deployment-metadata-rayfindeploymentsjson] Each deployment is recorded in `rayfin/.deployments.json` — a registry of every workspace you have deployed this project to: ```json title="rayfin/.deployments.json" { "active": "myworkspace", "deployments": { "myworkspace": { "fabricItemId": "7db00cb9-f630-4ecf-8fc9-942e60af5d78", "fabricApiUrl": "https://...", "fabricWorkspaceId": "8b17cf64-3c12-46ac-a572-192732c32641", "fabricTenantId": "...", "publishableKey": "pk-nua-EHihY2jz71V65YB4", "fabricPortalUrl": "https://dxt.fabric.microsoft.com/", "hostingUrl": "https://silky-sand-4924b3ad1f-centraluseuap.webapp.rayfingwdev.com", "deployedAt": "2026-04-28T01:15:50.514Z" } } } ``` The three fields you'll reach for most often: * **`fabricItemId`** — the Fabric item ID for this deployment. * **`hostingUrl`** — the public URL your static content is served from. * **`publishableKey`** — the public key `RayfinClient` uses to authenticate. It cannot be modified; it is retrieved from the remote service on first deploy. This file is not committed to source control (it's gitignored, like `rayfin/.env`) — it's regenerated per machine and per deployment target. ### Redirect URIs are updated automatically [#redirect-uris-are-updated-automatically] When static hosting is enabled, deploying registers your hosting URL's bare origin in `allowedRedirectUris` in `rayfin.yml` automatically — this is required for the Fabric-brokered auth handoff. You do not need to add it by hand. See [Redirect URIs](/docs/hosting/redirect-uris) for the full mechanics. ## Sign out [#sign-out] Clear cached credentials when you're done, or need to switch accounts: ```bash npx rayfin logout ``` ## Troubleshooting [#troubleshooting] See [Deployment troubleshooting](/docs/deploy/troubleshooting) for fixes to the most common deploy failures — expired sessions, keychain errors, blocked schema changes, and more. --- --- title: "Secrets" description: "Set API keys and tokens on a deployed Fabric app with rayfin secret set, so they stay server-side and never reach client code." url: https://rayfin.ai/docs/deploy/secrets markdown_url: https://rayfin.ai/docs/deploy/secrets.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: deploy/secrets.mdx --- # Secrets > Set API keys and tokens on a deployed Fabric app with rayfin secret set, so they stay server-side and never reach client code. Rayfin stores application secrets — API keys, third-party tokens, connection strings — on the deployed app's workload, separately from the public configuration in `rayfin.yml`. They are held server-side and never appear in client code or logs. > [!WARNING] > A **publishable key** (`pk-...`, the value in `rayfin/.deployments.json` and > `RAYFIN_PUBLIC_PUBLISHABLE_KEY`) is safe to ship in client code — it identifies your > project, not a credential. A **service secret** is not. Never put one in a > `RAYFIN_PUBLIC_*` variable, in frontend code, or anywhere a browser can read it. ## Deploy first [#deploy-first] Secrets attach to an existing deployment, so deploy your project before setting any: ```bash npx rayfin up ``` ## Set a secret [#set-a-secret] ```bash npx rayfin secret set API_KEY ``` The CLI prompts for the value with masked input, so it never lands in your shell history or in a file. The name you pass becomes the secret's name. Repeat for each secret you need. ## List secrets [#list-secrets] ```bash npx rayfin secret list ``` Returns names and timestamps only — values are never readable once set. See [`rayfin secret`](/docs/reference/cli/secret) for the full command reference. ## Rotating a secret [#rotating-a-secret] Set it again with the same name. The new value replaces the old one: ```bash npx rayfin secret set API_KEY ``` ## Current limitations [#current-limitations] > [!IMPORTANT] > `rayfin secret set` is **interactive only**. It reads the value from a masked prompt and > refuses to run when `CI=true` or when stdin is not a TTY, so there is no supported way to > set secrets from a CI pipeline or a script today. There is also no bulk import: secrets are set one at a time, by name. Plan for a manual step after the first deploy of a new environment. ```prompt title="Set up secrets on a deployed Rayfin app" My Rayfin app is deployed to Microsoft Fabric. Walk me through setting its secrets. Run `npx rayfin secret list` first to show what is already set. Then, for each secret I name, run `npx rayfin secret set ` and let me type the value at the masked prompt — do not ask me to paste secret values into the chat, and do not put them in any file or command argument. Confirm afterwards with `npx rayfin secret list`. ``` ## Reading a secret from your code [#reading-a-secret-from-your-code] Secrets are exposed to server-side code — [functions](/docs/functions) — not to the browser. Never plumb one through a `RAYFIN_PUBLIC_*` variable to reach the frontend; that is exactly the boundary those variables mark. ## Troubleshooting [#troubleshooting] ### The command refuses to prompt [#the-command-refuses-to-prompt] **Symptom:** `rayfin secret set` exits instead of asking for a value. **Cause:** stdin is not a TTY, or `CI=true` is set. The command has no non-interactive mode. **Fix:** run it from an interactive terminal. ### Authentication failed [#authentication-failed] **Symptom:** "Failed to acquire authentication token". **Cause:** you are not signed in, or the machine has no OS credential storage. **Fix:** run `npx rayfin login`. On containers or restricted environments, add `--encryption-fallback-enabled`. ### Permission denied [#permission-denied] **Symptom:** a permission error when setting or listing secrets. **Cause:** the signed-in account does not have access to the target Fabric workspace. **Fix:** confirm you are signed in with an account that has workspace access. Run `npx rayfin login` again — the account picker is always shown, so you can select a different account or tenant. --- --- title: "Deployment troubleshooting" description: "Symptom-to-fix reference for the most common failures when deploying a Rayfin project to Microsoft Fabric." url: https://rayfin.ai/docs/deploy/troubleshooting markdown_url: https://rayfin.ai/docs/deploy/troubleshooting.md section: deploy product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: deploy/troubleshooting.mdx --- # Deployment troubleshooting > Symptom-to-fix reference for the most common failures when deploying a Rayfin project to Microsoft Fabric. Each section below is a symptom you might hit while deploying a Rayfin project, why it happens, and how to fix it. If you don't see your error here, re-run the failing command with `-v, --verbose` for more detail. ## Deployment fails with 401 or 403 [#deployment-fails-with-401-or-403] **Cause:** your sign-in session has expired. **Fix:** re-authenticate and retry: ```bash npx rayfin login npx rayfin up ``` ## Sign-in fails with a keychain or credential-storage error [#sign-in-fails-with-a-keychain-or-credential-storage-error] **Cause:** some environments — certain Linux distributions, dev containers, and GitHub Codespaces — don't provide OS-backed credential storage, which `rayfin login` normally uses to store tokens securely. **Fix:** pass `--encryption-fallback-enabled` (or set `RAYFIN_ENCRYPTION_FALLBACK_ENABLED=true`) to allow a plaintext token cache instead. Use this only in development environments, not on shared or production machines: ```bash npx rayfin login --encryption-fallback-enabled ``` ## Deploy fails with "Dialect is required when Data module is enabled" [#deploy-fails-with-dialect-is-required-when-data-module-is-enabled] **Cause:** `services.data.enabled: true` is set in `rayfin.yml` without a `dialect`. This produces a 400 error at deploy time. **Fix:** add `dialect: mssql` under `services.data` — Fabric apps support MSSQL only: ```yaml title="rayfin/rayfin.yml" services: data: enabled: true dialect: mssql ``` ## Database apply reports destructive changes [#database-apply-reports-destructive-changes] **Cause:** `rayfin up db apply` blocks schema changes that could cause data loss — for example, dropping a column or a table. **Fix:** review the listed operations. If you accept the data loss, re-run with `--force`: ```bash npx rayfin up db apply --force ``` ## Static deploy exceeds the size limit [#static-deploy-exceeds-the-size-limit] **Cause:** the compressed static content archive exceeds the 100 MB limit for `rayfin up` and `rayfin up staticapp deploy`. **Fix:** exclude source maps and large development assets from your production build, or move binary files to Rayfin storage instead of bundling them as static content. ## No remote endpoint configured [#no-remote-endpoint-configured] **Cause:** `rayfin up staticapp deploy` requires an existing deployment to upload to. **Fix:** run `npx rayfin up` first to create the Fabric app, then use `staticapp deploy` for subsequent static-only updates. ## GraphQL "Internal server error" after a successful deploy [#graphql-internal-server-error-after-a-successful-deploy] **Cause:** an entity has a `@text()` field without a `max` option. On MSSQL this generates an `NVARCHAR(MAX)` column, which can prevent the metadata provider from building a GraphQL schema — the deploy itself reports success, but the API fails at runtime. **Fix:** add an explicit `max` to every string field, then push the schema change: ```typescript title="rayfin/data/Todo.ts" @text({ max: 200 }) title!: string; ``` ```bash npx rayfin up db apply --force ``` ## A new entity is unreadable after a deploy that reported success [#a-new-entity-is-unreadable-after-a-deploy-that-reported-success] **Cause:** the static app and settings deployed correctly, but the schema for the new entity was not applied — usually because the entity was added after the last successful `db apply`, or a prior apply was skipped. **Fix:** confirm the deployment is otherwise healthy, then explicitly (re)apply the schema: ```bash npx rayfin up status npx rayfin up db apply ``` ```prompt title="Diagnose a failed Rayfin deployment" My Rayfin project's deployment to Microsoft Fabric isn't working as expected — either `rayfin up` failed, or it succeeded but the app is behaving incorrectly at runtime. Run `npx rayfin up status` to check deployment health, then re-run `npx rayfin up -v` for verbose output and show me the error. If it's a schema problem — a new or changed entity not showing up, or a GraphQL "Internal server error" — check rayfin/data/*.ts for @text() fields missing a max option, and for a missing dialect under services.data in rayfin.yml. Then run `npx rayfin up db apply` (adding --force only if the change is expected to be destructive) and confirm the fix with `npx rayfin up status` again. ``` ## See also [#see-also] * [Secrets](/docs/deploy/secrets) — troubleshooting for `rayfin secret set` specifically. * [Known limitations](/docs/reference/known-limitations) — current platform constraints that aren't bugs. --- --- title: "Aggregations" description: "Compute sums, averages, minimums, maximums, and counts over your entities with groupBy() and aggregate(), including grand totals and having filters." url: https://rayfin.ai/docs/data/aggregations markdown_url: https://rayfin.ai/docs/data/aggregations.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: data/aggregations.mdx --- # Aggregations > Compute sums, averages, minimums, maximums, and counts over your entities with groupBy() and aggregate(), including grand totals and having filters. Aggregations compute a value **across many rows** on the server instead of fetching rows and reducing them in the browser. Use them for dashboard tiles, report rollups, and any number that would otherwise require reading a whole table. ```typescript const byRegion = await rayfinClient.data.Order.groupBy(['region']) .aggregate({ revenue: { sum: 'amount' }, orders: { count: 'amount' }, }) .execute(); for (const row of byRegion) { console.log(row.fields.region, row.aggregations.revenue, row.aggregations.orders); } ``` > [!IMPORTANT] > Every aggregation operation — including `count` — accepts **numeric fields only**. See > [Only numeric fields aggregate](#only-numeric-fields-aggregate) before you reach for > `count` to count rows. ## The aggregation chain [#the-aggregation-chain] An aggregation is `groupBy()` then `aggregate()` then `execute()`. `where()` is optional and comes first: ```typescript const rows = await rayfinClient.data.Order .where({ status: { eq: 'shipped' } }) .groupBy(['region']) .aggregate({ revenue: { sum: 'amount' } }) .execute(); ``` `groupBy()` takes the scalar fields to group on. `aggregate()` takes a map of **aliases you choose** to single-operation entries. `execute()` returns one row per group. ## Result shape [#result-shape] Every result row has two halves — the grouped column values, and the aggregated values keyed by your aliases: ```typescript type Row = { fields: { region: string }; aggregations: { revenue: number | null; orders: number }; }; ``` `fields` carries the values you grouped on, deserialized to your declared entity types (a `@date()` column comes back as a `Date`, not an ISO string). `aggregations` carries one entry per alias. `count` is always a `number`. `sum`, `avg`, `min`, and `max` are `number | null`, because SQL returns `NULL` over a group with no non-null values. ## Grand totals [#grand-totals] Skip `groupBy()` to aggregate over the entire filtered set. You get exactly one row, and its `fields` is an empty object: ```typescript const [totals] = await rayfinClient.data.Order .where({ status: { eq: 'shipped' } }) .aggregate({ revenue: { sum: 'amount' }, average: { avg: 'amount' }, largest: { max: 'amount' }, }) .execute(); totals.aggregations.revenue; // number | null totals.fields; // {} ``` ## Operations [#operations] | Operation | Returns | Meaning | | --------- | ---------------- | ------------------------------------ | | `sum` | `number \| null` | Total of the field across the group | | `avg` | `number \| null` | Mean of the field across the group | | `min` | `number \| null` | Smallest value in the group | | `max` | `number \| null` | Largest value in the group | | `count` | `number` | Count of numeric values in the field | Each alias must specify **exactly one** operation. Two operations under one alias is a compile-time error — give each its own alias instead: ```typescript // Correct — one operation per alias. .aggregate({ revenue: { sum: 'amount' }, average: { avg: 'amount' }, }) ``` ## Only numeric fields aggregate [#only-numeric-fields-aggregate] Data API Builder generates every aggregation's `field` argument as the entity's `NumericAggregateFields` enum. That applies to `count` too — it is a *count of numeric values*, not a count of rows. ```typescript // Compile error: 'status' is a text field. .aggregate({ n: { count: 'status' } }) ``` So `count` gives you a row count only when you point it at a **non-nullable numeric column** that every row populates. On an entity with no numeric column, there is still no row count — select the minimal field set and use `results.length`, or page through with `.executePaginated()` and sum `page.items.length`. See [Pagination](/docs/data/querying#paginate-large-lists). ## Filter aggregated values with `having` [#filter-aggregated-values-with-having] The long form of an operation is `{ field, having?, distinct? }`. `having` filters on the **aggregated** value, using the same numeric operators as `.where()`: ```typescript const bigRegions = await rayfinClient.data.Order.groupBy(['region']) .aggregate({ revenue: { sum: { field: 'amount', having: { gt: 10000 } } }, }) .execute(); ``` `where()` filters rows *before* grouping; `having` filters values *after* aggregation. Use both together when you need each. ## Count distinct values [#count-distinct-values] `distinct: true` aggregates only distinct values of the field: ```typescript const rows = await rayfinClient.data.Order.groupBy(['region']) .aggregate({ uniqueAmounts: { count: { field: 'amount', distinct: true } }, }) .execute(); ``` ## Group on several fields [#group-on-several-fields] `groupBy()` accepts multiple fields, and each appears in `fields` on the result: ```typescript const rows = await rayfinClient.data.Order.groupBy(['region', 'status']) .aggregate({ revenue: { sum: 'amount' } }) .execute(); rows[0].fields.region; rows[0].fields.status; ``` Duplicate fields are collapsed, and order is preserved. ## What you cannot combine [#what-you-cannot-combine] Data API Builder rejects a query that asks for grouped aggregates **and** rows at the same time, so `aggregate()` is mutually exclusive with the row-shaping methods. Each of these is a compile-time error, backed by a runtime guard: | Combination | Why it fails | | ------------------------------- | ----------------------------------------------------------- | | `.select(...).aggregate(...)` | Row selection and grouped aggregation are different queries | | `.first(n).aggregate(...)` | Grouped aggregation does not paginate rows | | `.after(cursor).aggregate(...)` | Same — no row pagination | | `.orderBy(...).aggregate(...)` | Row ordering does not apply to groups | | `.groupBy(...).execute()` | Grouping without `aggregate()` returns nothing useful | Sort or slice the returned array in your own code instead — one row per group is normally a small result. ## Aliases and field names [#aliases-and-field-names] Aliases and field names are emitted into the GraphQL document as bare tokens, so both must match the GraphQL name grammar (`/^[_A-Za-z][_0-9A-Za-z]*$/`) and must not start with `__`. An alias like `total revenue` or `2024` is rejected before the request is sent. Stick to identifiers you would use as a TypeScript property name. ## Aggregating connector entities [#aggregating-connector-entities] Category A [connector](/docs/connectors) entities expose the same `groupBy()` and `aggregate()` methods with identical behavior: ```typescript const rows = await client.connectors.sales.Order.groupBy(['region']) .aggregate({ revenue: { sum: 'total' } }) .execute(); ``` See [Fabric SQL sources](/docs/connectors/sql-sources) for the rest of the connector query surface. ```prompt title="Add an aggregated dashboard query" In my Rayfin project, add a function that returns dashboard totals from the RayfinClient using the aggregation API rather than fetching rows and reducing them in JavaScript. Group with .groupBy([...]) on the scalar fields I want to break the numbers down by, then call .aggregate({ alias: { sum | avg | min | max | count: 'field' } }) and .execute(). Use one operation per alias. Remember that every operation, including count, accepts numeric fields only, so do not try to count a text or uuid column. Type the result as { fields, aggregations }[], and treat sum/avg/min/max as number | null since SQL returns NULL over empty groups. Do not combine .aggregate() with .select(), .orderBy(), .first() or .after() — those are mutually exclusive with grouped aggregation. Sort the returned array in TypeScript instead. ``` --- --- title: "Field types" description: "Complete reference for Rayfin's field decorators — @uuid, @text, @int, @decimal, @boolean, @date, @email, @set, and @blob — and the options each accepts." url: https://rayfin.ai/docs/data/field-types markdown_url: https://rayfin.ai/docs/data/field-types.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: data/field-types.mdx --- # Field types > Complete reference for Rayfin's field decorators — @uuid, @text, @int, @decimal, @boolean, @date, @email, @set, and @blob — and the options each accepts. Every property on a [`@entity()`](/docs/data/modeling) class needs exactly one field decorator. The decorator determines the database column type, the GraphQL scalar, and the constraints Rayfin enforces for you. This page lists every decorator, the options it accepts, and the patterns that trip people up — especially the MSSQL text length rule. ## Decorator reference [#decorator-reference] | Decorator | Logical type | Extra options beyond the common set | Notes | | ----------------- | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | `@uuid()` | UUID | — | Conventionally the primary key (`id`). Also used for foreign key columns. | | `@text()` | string | `max`, `min`, `regex` | **Always set `max` on MSSQL** — see below. | | `@int()` | integer | `max`, `min` | Whole numbers. | | `@decimal()` | decimal / numeric | `max`, `min`, `precision`, `scale` | Defaults to `DECIMAL(18,2)`. | | `@boolean()` | boolean | — | True/false. | | `@date()` | datetime | — | Accepts a `Date`, an ISO string, or a numeric timestamp on write; serializes as ISO on read. | | `@email()` | string | `max`, `min`, `regex` | Text field with email-shaped validation. Same option set as `@text()`. | | `@set(...values)` | string enum | — | A fixed list of allowed string literals. | | `@blob()` | — | — | A **class** decorator for storage folders, not a field type. See [below](#blob-storage-folders). | Every field decorator except `@blob()` also accepts the common options in the next section. ## Common options [#common-options] These apply to `@uuid()`, `@text()`, `@int()`, `@decimal()`, `@boolean()`, `@date()`, `@email()`, and `@set()`: | Option | Type | Default | Effect | | ------------- | ------------------------ | ------- | --------------------------------------------------------------------------------------------------------------- | | `optional` | `boolean` | `false` | Allows `NULL` in the database. Pair with a `?` on the TypeScript property — see [below](#the-nullable-pattern). | | `unique` | `boolean` | `false` | Adds a unique constraint on the column. | | `default` | matches the field's type | — | Default value used when the field is omitted on create. | | `description` | `string` | — | Free-text note attached to the field's metadata. Does not change validation or the database column. | ```typescript title="rayfin/data/Todo.ts" import { entity, authenticated, uuid, text, boolean, date } from '@microsoft/rayfin-core'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @boolean({ default: false }) isCompleted!: boolean; @date() createdAt!: Date; @text({ max: 128, description: 'Owning user, set from claims.sub on create' }) user_id!: string; } ``` ## The nullable pattern [#the-nullable-pattern] Fields are **required by default**. To make a field nullable, you must do two things together — set `{ optional: true }` in the decorator **and** mark the TypeScript property with `?`. Either one alone is not enough for consistent behavior between the database constraint and the generated types. ```typescript title="rayfin/data/Todo.ts" @text() title!: string; // required @text({ optional: true }) notes?: string; // nullable — both the option and `?` are present ``` ## Text length and MSSQL [#text-length-and-mssql] > [!WARNING] > On MSSQL, a `@text()` field without `max` generates an `NVARCHAR(MAX)` column. Rayfin's > metadata provider can fail to build a GraphQL schema from `NVARCHAR(MAX)` columns, > producing an "Internal server error" at runtime — after `rayfin up` has already reported > success. Always set `max` on every `@text()` field: `@text({ max: 200 })`. This applies > to `@email()` too, since it shares `@text()`'s option set. There is no safe default length to omit — pick a `max` that fits the data (`50` for a short name, `2000` for a description, and so on). If a deploy succeeds but GraphQL queries against a new or changed entity start failing, check every `@text()` field on that entity for a missing `max` first. ## `@decimal()` precision and scale [#decimal-precision-and-scale] `precision` is the total number of digits (before and after the decimal point); `scale` is the number of digits after it. They must be provided together — if you set one, set the other. Omit both to get the default, `DECIMAL(18,2)`. Maximum precision is 28, a limit imposed by the Data API Builder runtime. ```typescript title="rayfin/data/Product.ts" import { entity, authenticated, uuid, decimal } from '@microsoft/rayfin-core'; @entity() @authenticated('*') export class Product { @uuid() id!: string; @decimal() price!: number; // DECIMAL(18,2) by default @decimal({ precision: 10, scale: 4 }) weight!: number; // DECIMAL(10,4) } ``` ## `@set()` string enums [#set-string-enums] `@set()` takes the allowed values as separate string arguments, and the TypeScript union type should match: ```typescript title="rayfin/data/Todo.ts" @set('low', 'medium', 'high') priority!: 'low' | 'medium' | 'high'; ``` To add common options (`optional`, `unique`, `default`) to a set field, pass an options object as the first argument instead, followed by the allowed values: ```typescript title="rayfin/data/Todo.ts" @set({ optional: true }, 'low', 'medium', 'high') priority?: 'low' | 'medium' | 'high'; ``` ## `@blob()` storage folders [#blob-storage-folders] > [!WARNING] > Storage is experimental and is not available in every Fabric region or tenant. See > [Storage](/docs/storage) before you depend on it. `@blob()` is a **class-level** decorator, structurally different from the field decorators above — it marks a class as a storage folder configuration for `@microsoft/rayfin-storage`, the same way `@entity()` marks a class as a database table. It does not take `optional`, `default`, `max`, or `unique` options itself, and the properties inside the class are plain TypeScript fields rather than `@text()` / `@uuid()` decorated columns: ```typescript title="rayfin/storage/ProfileImage.ts" import { blob, role } from '@microsoft/rayfin-core'; @blob('uploads') @role('authenticated', '*', { policy: (claims, item) => claims.sub.eq(item.owner_id), }) export class ProfileImage { owner_id!: string; } ``` The string argument (`'uploads'` above) is the storage folder name; it defaults to the kebab-case class name if omitted. Permissions on a `@blob()` class use the same `@role()` / `@anonymous()` / `@authenticated()` decorators described in [Permissions and row-level security](/docs/data/permissions), but they generate a storage policy instead of a database policy. File upload, download, and listing operations are part of the storage client, not the `client.data.` API this section covers. ```prompt title="Add a validated field to an entity" In my Rayfin project, add an "email" field to the User entity in rayfin/data/User.ts using the @email() decorator with max: 254 and unique: true. Make sure the TypeScript property is required (no `?`), since the field should not be nullable. ``` --- --- title: "Data" description: "Model entities once as decorated TypeScript classes and get a database schema, GraphQL API, type-safe client, permissions, and validation from the same source." url: https://rayfin.ai/docs/data markdown_url: https://rayfin.ai/docs/data.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: data/index.mdx --- # Data > Model entities once as decorated TypeScript classes and get a database schema, GraphQL API, type-safe client, permissions, and validation from the same source. A Rayfin entity is a TypeScript class. Decorate it once and Rayfin generates the database table, a GraphQL API, a type-safe client, row-level security, and form validation from that single definition — there is no separate schema file, migration file, or API contract to keep in sync by hand. ```typescript title="rayfin/data/Todo.ts" import { entity, authenticated, uuid, text, boolean, date } from '@microsoft/rayfin-core'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @boolean({ default: false }) isCompleted!: boolean; @date() createdAt!: Date; @text({ max: 128 }) user_id!: string; } ``` Register it in `schema.ts`, then apply it: ```bash npx rayfin up ``` From there, `client.data.Todo` is a fully typed read/write API, scoped by the row-level policy declared above. ## Set up the client [#set-up-the-client] Construct one `RayfinClient` and reuse it across your app. Type it with your `AppSchema` so every entity access is fully typed. ```typescript title="src/services/rayfinClient.ts" import { RayfinClient } from '@microsoft/rayfin-client'; import type { AppSchema } from '../../rayfin/data/schema'; export const rayfinClient = new RayfinClient({ baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY ?? '', }); ``` `VITE_RAYFIN_API_URL` and `VITE_RAYFIN_PUBLISHABLE_KEY` are generated into `.env.local` by `rayfin up` — see [Schema changes](/docs/data/migrations) for the apply workflow that produces them. That one client also carries [`client.auth`](/docs/auth) and, when enabled, [`client.functions`](/docs/functions/calling-functions). To additionally reach existing Fabric data sources, swap it for `ConnectorsRayfinClient` — see [Wiring connectors into your app](/docs/connectors/client-setup). ## Model your schema [#model-your-schema] * **[Modeling entities](/docs/data/modeling)** — `@entity()`, file layout, primary keys, and the `schema.ts` registration step. * **[Field types](/docs/data/field-types)** — every field decorator and its options, including the MSSQL text-length rule. * **[Relationships](/docs/data/relationships)** — `@one()` / `@many()`, foreign key columns, and the many-to-many workaround. * **[Permissions](/docs/data/permissions)** — `@role()`, `@anonymous()`, `@authenticated()`, and the row-level policy DSL. ## Read and write [#read-and-write] * **[Querying](/docs/data/querying)** — the `select` / `where` / `orderBy` / `execute` chain, filtering, and pagination. * **[Aggregations](/docs/data/aggregations)** — sums, averages, and counts computed on the server with `groupBy()` and `aggregate()`. * **[Creating, updating, deleting](/docs/data/mutations)** — writes, and setting relationships correctly. * **[Form validation](/docs/data/validation)** — generate form validation from the same entity, with no separate schema library. ## Operate [#operate] * **[Schema changes](/docs/data/migrations)** — how edits to `rayfin/data/` reach the database, and how to verify they actually did. * **[Seeding data](/docs/data/seeding)** — populate a database from a Node.js script. ```prompt title="Model your first entity" In my Rayfin project, create a new entity in rayfin/data/ that models [describe your data]. Give it a uuid id, appropriate field decorators from @microsoft/rayfin-core with explicit max lengths on every text field, and an @authenticated('*') permission decorator with a policy scoping rows to the signed-in user via claims.sub. Register it in rayfin/data/schema.ts, then apply the schema with `rayfin up`. ``` --- --- title: "Schema changes" description: "Apply Rayfin entity changes to the database with rayfin up and rayfin up db apply, and verify the schema actually reached the server." url: https://rayfin.ai/docs/data/migrations markdown_url: https://rayfin.ai/docs/data/migrations.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: data/migrations.mdx --- # Schema changes > Apply Rayfin entity changes to the database with rayfin up and rayfin up db apply, and verify the schema actually reached the server. Editing a class in `rayfin/data/` does not change anything by itself — Rayfin only reads your entities and generates Data API Builder (DAB) configuration when you explicitly apply it. This page covers the commands that do that, and how to verify the apply actually took effect. ## `rayfin up` is the canonical command [#rayfin-up-is-the-canonical-command] `npx rayfin up` is the command to reach for any time you want your latest entity changes live on your deployed Fabric app. It is not schema-only: it syncs runtime settings, **applies the database schema generated from your `rayfin/data` decorators**, and builds and deploys static content if `staticHosting` is enabled, all in one step. Run it after every change to a file under `rayfin/data/`. ```bash npx rayfin up ``` Running it again after the first deploy updates the same deployment rather than creating a new one, so it is safe to run repeatedly as you iterate. ## `rayfin up db apply`: the schema-only escape hatch [#rayfin-up-db-apply-the-schema-only-escape-hatch] `npx rayfin up db apply` generates and applies **only** the database schema, without touching runtime settings or static content. It is an advanced subcommand — reach for it when you specifically want to push a schema change without re-running the full `rayfin up` flow (for example, while iterating locally with `npm run dev` serving your frontend and the backend already deployed). ```bash npx rayfin up db apply ``` If the change could cause data loss — dropping a column, narrowing a type, renaming a table — the CLI blocks it and explains what it found. Add `--force` once you have reviewed the listed operations and accept the loss: ```bash npx rayfin up db apply --force ``` ## Verify the change actually reached the server [#verify-the-change-actually-reached-the-server] > [!WARNING] > A `rayfin up` (or `rayfin up db apply`) that reports success does not guarantee your > frontend can immediately read a newly added or changed entity. The verified failure > mode: you add an entity, the deploy command prints success, and GraphQL queries against > that entity still fail at runtime — because the schema change had not actually finished > applying when you tested it. Treat verification as a required step, not an assumption: > > 1. After any change to a file in `rayfin/data/`, run `rayfin up status` and confirm the > deployment reports healthy before exercising the new entity from your app. > 2. If a new or changed entity still returns GraphQL errors despite a successful deploy, > run `rayfin up db apply` explicitly (add `--force` if it reports a potentially > destructive change), then retest. ```bash npx rayfin up status ``` Add `--json` for machine-readable output, useful in a script that waits for a healthy deployment before running further checks: ```bash npx rayfin up status --json ``` ## Typical workflow [#typical-workflow] ```bash # 1. Edit rayfin/data/Todo.ts, add a field or a new entity # 2. Apply the change npx rayfin up # 3. Verify the deployment is healthy npx rayfin up status # 4. If a new entity errors at runtime despite a successful deploy, apply schema explicitly npx rayfin up db apply --force ``` ## Troubleshooting [#troubleshooting] **GraphQL returns "Internal server error" after a successful deploy** — check every `@text()` field on the affected entity for a missing `max`. On MSSQL, `@text()` without `max` generates an `NVARCHAR(MAX)` column, which can break GraphQL schema generation. Add explicit `max` values (see [Field types](/docs/data/field-types#text-length-and-mssql)) and redeploy with `npx rayfin up db apply --force`. **`rayfin up db apply` reports a potentially destructive change** — review the listed operations (dropped columns, narrowed types, renamed tables). Re-run with `--force` only once you have confirmed the data loss is acceptable. **`rayfin up db apply` fails outright** — wait for services to report healthy (`rayfin up status`) before retrying. **Enabling `data` without a `dialect`** — `rayfin.yml` requires `dialect: mssql` whenever `services.data.enabled` is `true`. Omitting it causes a 400 error at apply time. ```prompt title="Apply and verify a schema change" I just added a new field to an entity in my Rayfin project's rayfin/data/ folder. Run `rayfin up` to apply the change, then run `rayfin up status` to confirm the deployment is healthy. If querying the changed entity still fails after that, run `rayfin up db apply --force` and check again. ``` --- --- title: "Modeling entities" description: "Define Rayfin entities as decorated TypeScript classes in rayfin/data/ and register them in schema.ts to get a database table and a typed API." url: https://rayfin.ai/docs/data/modeling markdown_url: https://rayfin.ai/docs/data/modeling.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: data/modeling.mdx --- # Modeling entities > Define Rayfin entities as decorated TypeScript classes in rayfin/data/ and register them in schema.ts to get a database table and a typed API. A Rayfin entity is a TypeScript class decorated with `@entity()`. Rayfin reads the class at build time and generates a database table, a REST endpoint, a GraphQL endpoint, and a typed client method for it — you never write SQL or a schema file by hand. This page covers the mechanics of defining an entity: where the file lives, how the primary key works, how to register the entity so the client can see it, and the TypeScript compiler settings the decorators require. For the full list of field decorators, see [Field types](/docs/data/field-types). For foreign keys and navigation properties, see [Relationships](/docs/data/relationships). ## Where entity files live [#where-entity-files-live] Entities live in `rayfin/data/`, one class per file. The Rayfin CLI scans this folder when you run `rayfin up` or `rayfin up db apply`. ```text your-project/ ├── rayfin/ │ ├── data/ │ │ ├── Todo.ts │ │ └── schema.ts │ ├── rayfin.yml │ └── tsconfig.json ├── src/ └── tsconfig.json ``` ## Define an entity [#define-an-entity] Add `@entity()` to a class and a field decorator to every property you want stored in the database. Import decorators from `@microsoft/rayfin-core`. ```typescript title="rayfin/data/Todo.ts" import { entity, authenticated, uuid, text, boolean, date, } from '@microsoft/rayfin-core'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @boolean({ default: false }) isCompleted!: boolean; @date() createdAt!: Date; @text({ max: 128 }) user_id!: string; } ``` This single class produces: * A `todos` table (Rayfin pluralizes the class name for the source table). * A GraphQL entity reachable as `client.data.Todo` from the typed client. * A row-level security policy restricting every action to the row's owner (see [Permissions and row-level security](/docs/data/permissions)). Every entity needs an explicit permission decorator — `@authenticated(...)` here. An entity with none of `@role()`, `@anonymous()`, or `@authenticated()` silently receives full CRUD access for any signed-in user. See [the warning on the permissions page](/docs/data/permissions#entities-without-a-permission-decorator) before you ship anything. ## Entity primary key [#entity-primary-key] Every entity has a UUID `string` primary key named `id`. * If you omit `id` from the class body, Rayfin adds it to the schema automatically. * Declaring it explicitly (`@uuid() id!: string;`) is optional but recommended — it keeps the TypeScript type complete and makes the field visible to tooling. * `id` is **optional when creating a record**: omit it and the server generates a UUID: \`. Supply your own UUID at creation time if you need a client-generated identifier. * Composite primary keys, or primary keys on a field other than `id`, are not supported. ```typescript title="rayfin/data/Category.ts" import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @authenticated('*') export class Category { @uuid() id!: string; // UUID primary key — auto-generated when omitted on create @text({ max: 100 }) name!: string; } ``` ## Register the entity in `schema.ts` [#register-the-entity-in-schemats] `rayfin/data/schema.ts` maps entity names to their classes. `RayfinClient` uses this map to type `client.data.` and to resolve relationship targets. Add every new entity to both the value array and the exported type. ```typescript title="rayfin/data/schema.ts" import { Todo } from './Todo.js'; export type AppSchema = { Todo: Todo; }; export const schema = [Todo]; ``` When you add a second entity, extend both the type and the array: ```typescript title="rayfin/data/schema.ts" import { Todo } from './Todo.js'; import { Category } from './Category.js'; export type AppSchema = { Todo: Todo; Category: Category; }; export const schema = [Todo, Category]; ``` Import the `AppSchema` type wherever you construct a `RayfinClient` — see [Querying](/docs/data/querying) for client setup. > [!NOTE] > Use `.js` extensions on relative imports between files under `rayfin/data/` (for example > `from './Todo.js'`), even though the source files are `.ts`. This matches the emitted > ESM output and is required for the imports to resolve at runtime. ## The TC39 decorator requirement [#the-tc39-decorator-requirement] Rayfin entities use TC39 Stage 3 decorators — the same decorator syntax now standard in TypeScript — not the legacy experimental decorators used by older frameworks. This has two concrete consequences for your `tsconfig.json`: * **Never set `experimentalDecorators: true`.** Rayfin's decorators are incompatible with the legacy decorator model. * **Never set `emitDecoratorMetadata: true`.** TypeScript only allows this alongside `experimentalDecorators`, so enabling it breaks the same way. * **Add `ESNext.Decorators` to the `lib` array.** This is what actually enables TC39 decorator type-checking. ```json title="tsconfig.json" { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable", "ESNext.Decorators"], "module": "ESNext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["src"], "references": [{ "path": "./rayfin" }] } ``` The `references` entry points at `rayfin/tsconfig.json`, a project-reference config the CLI uses to compile your entity definitions. It extends your root config and sets `composite: true`; you should not need to edit it directly. Projects scaffolded with `npm create @microsoft/rayfin@latest` already have these settings. If you are adding Rayfin to an existing project, verify your `tsconfig.json` matches the example above. > [!WARNING] > If your frontend build tool compiles independently of `tsc` (for example Vite with > esbuild), it must also target ES2022 or later, or decorator syntax fails to parse. Set > `target: 'es2022'` under `build`, `esbuild`, and `optimizeDeps.esbuildOptions` in > `vite.config.ts`, and use the default `@vitejs/plugin-react` (esbuild-based) plugin — > `@vitejs/plugin-react-swc` cannot parse TC39 decorators at any target setting and fails > with `Expression expected`. ## Applying your changes [#applying-your-changes] Defining a class does not change the database by itself. Schema changes are picked up the next time you run `rayfin up` (or the narrower `rayfin up db apply`). See [Schema changes](/docs/data/migrations) for the full apply and verification workflow. ## Best practices [#best-practices] * Start every entity with an explicit permission decorator — never rely on the default. * Include a `user_id` field on any entity scoped to the signed-in user, and pair it with a `policy` that compares it to `claims.sub` (see [Permissions and row-level security](/docs/data/permissions)). * Always set `max` on `@text()` fields — see [Field types](/docs/data/field-types#text-length-and-mssql) for why. * Keep one entity class per file, named after the class, so the CLI's file scan and your imports stay predictable. ```prompt title="Add a new entity" In my Rayfin project, add a new entity called Project in rayfin/data/Project.ts with fields: name (text, max 150), description (optional text, max 2000), isArchived (boolean, default false), createdAt (date), and user_id (text, max 128) for ownership. Add an @authenticated('*') permission decorator with a policy comparing claims.sub to item.user_id. Register the entity in rayfin/data/schema.ts by adding it to both the AppSchema type and the schema array. Then apply the schema change with `rayfin up`. ``` --- --- title: "Creating, updating, deleting" description: "Create, update, and delete Rayfin records through the type-safe client, and set relationship fields correctly in mutations." url: https://rayfin.ai/docs/data/mutations markdown_url: https://rayfin.ai/docs/data/mutations.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: data/mutations.mdx --- # Creating, updating, deleting > Create, update, and delete Rayfin records through the type-safe client, and set relationship fields correctly in mutations. `client.data.` also handles writes. `create`, `update`, and `delete` are fully typed against your entity — including relationship fields defined with `@one()`. ## Create a record [#create-a-record] Pass every required field. `id` is optional — omit it and the server generates a UUID. ```typescript const todo = await rayfinClient.data.Todo.create({ title: 'Ship the changelog', isCompleted: false, createdAt: new Date(), user_id: session.user.id, }); ``` Supply your own `id` at creation time only if you specifically need a client-generated identifier — it is validated as a UUID like any other write. ## Update a record [#update-a-record] `update` takes a filter identifying the record, then the fields to change. Only `id` is supported in the filter. ```typescript await rayfinClient.data.Todo.update( { id: todo.id }, { isCompleted: true }, ); ``` Send only the fields that changed — `update` does a partial patch, not a full replace. ## Delete a record [#delete-a-record] ```typescript await rayfinClient.data.Todo.delete({ id: todo.id }); ``` `delete` resolves once the backend confirms the row is gone. ## Setting `@one()` relationships in mutations [#setting-one-relationships-in-mutations] For an entity with a `@one()` field (see [Relationships](/docs/data/relationships)), pass the **related object** — either the full object or an object containing just its `id` — never the raw foreign key column directly. ```typescript title="rayfin/data/Note.ts (relevant fields)" // @uuid() notebook_id!: string; // @one(() => Notebook, { optional: true }) notebook?: Notebook; ``` ```typescript // Correct — pass the relationship object, primary key only const note = await rayfinClient.data.Note.create({ title: 'Meeting notes', content: 'Discussion points…', createdAt: new Date(), notebook: { id: notebookId }, }); // Also correct — pass the full object if you already have it const notebook = await rayfinClient.data.Notebook.findFirst({ name: { eq: 'Work' } }); const note2 = await rayfinClient.data.Note.create({ title: 'Weekly summary', content: 'Use the full object when convenient', createdAt: new Date(), notebook, // full Notebook object }); ``` ```typescript // Wrong — do not set the generated foreign key column directly in a mutation await rayfinClient.data.Note.create({ title: 'Meeting notes', content: 'Discussion points…', createdAt: new Date(), notebook_id: notebookId, // not how relationships are set on write }); ``` Both the full-object and `{ id }` forms produce the same GraphQL mutation; the client converts whichever one you pass into the entity's foreign key field (`notebook_id`) internally. The same rule applies to `update`: ```typescript // Move a note to a different notebook by passing just the target's id await rayfinClient.data.Note.update( { id: note.id }, { notebook: { id: newNotebookId } }, ); ``` `@many()` fields are the inverse side of a relationship and are read-only in mutations — passing an array for a `@many()` field is ignored. Manage that side of the relationship by updating the `@one()` foreign key on the child records instead (set each child's `notebook: { id }` to reassign it, as shown above), not by writing to the parent's `@many()` collection. ## Full example [#full-example] ```typescript title="src/services/todos.ts" import { rayfinClient } from './rayfinClient'; export async function createTodo(title: string, userId: string) { return rayfinClient.data.Todo.create({ title, isCompleted: false, createdAt: new Date(), user_id: userId, }); } export async function completeTodo(id: string) { return rayfinClient.data.Todo.update({ id }, { isCompleted: true }); } export async function deleteTodo(id: string) { await rayfinClient.data.Todo.delete({ id }); } ``` ## Upsert [#upsert] `client.data.` also exposes `upsert(where, create, update)`: it applies `update` if a record matching `where` exists, or `create` otherwise. ```typescript await rayfinClient.data.Category.upsert( { id: categoryId }, { id: categoryId, name: 'Work' }, // used if no row with this id exists { name: 'Work' }, // used if it already exists ); ``` ```prompt title="Add a create-and-assign mutation" In my Rayfin project, write a function in src/services/notes.ts that creates a new Note using the RayfinClient. It should accept a title, content, and notebookId, set createdAt to the current time, and assign the note to its notebook by passing `notebook: { id: notebookId }` rather than setting a notebook_id field directly. ``` --- --- title: "Permissions and row-level security" description: "Secure Rayfin entities with @role, @anonymous, and @authenticated, including row-level policies, field visibility, action-specific rules, and multi-tenant scoping." url: https://rayfin.ai/docs/data/permissions markdown_url: https://rayfin.ai/docs/data/permissions.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: data/permissions.mdx --- # Permissions and row-level security > Secure Rayfin entities with @role, @anonymous, and @authenticated, including row-level policies, field visibility, action-specific rules, and multi-tenant scoping. Permissions are declared on the entity class itself, as decorators, and compiled into Data API Builder (DAB) configuration when you apply the schema. There is no separate permissions file to keep in sync with your models. ## Entities without a permission decorator [#entities-without-a-permission-decorator] > [!WARNING] > An entity with **no** `@role()`, `@anonymous()`, or `@authenticated()` decorator does > not become inaccessible — it silently receives `authenticated: *`, meaning **full > create/read/update/delete access for any signed-in user**, with no row-level > restriction. This is rarely what you want for real data. Add an explicit permission > decorator to every entity, even if it is as simple as `@authenticated('*')`. ## Built-in roles [#built-in-roles] Rayfin recognizes two built-in roles: * **`anonymous`** — public access, no authentication required. * **`authenticated`** — requires a valid signed-in session. > [!NOTE] > Anonymous access requires a tenant admin to enable the "Enable anonymous data access > for Fabric Apps" switch for your tenant. ## `@role()`, and the `@anonymous()` / `@authenticated()` shorthands [#role-and-the-anonymous--authenticated-shorthands] `@role()` is the general-purpose class decorator; `@anonymous()` and `@authenticated()` are shorthands for `@role('anonymous', ...)` and `@role('authenticated', ...)`. Prefer the shorthands — they read better and are what most of this page uses from here on. ```typescript @role(roleName, actions, options?) @anonymous(actions?, options?) // shorthand for @role('anonymous', ...) @authenticated(actions?, options?) // shorthand for @role('authenticated', ...) ``` | Parameter | Type | Description | | ---------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `roleName` | `'anonymous' \| 'authenticated'` | Which built-in role this rule applies to. Only present on `@role()` — the shorthands imply it. | | `actions` | `'create' \| 'read' \| 'update' \| 'delete' \| '*'`, or an array of those | Which operations this rule grants. `'*'` means all four. Defaults to `'*'` if omitted. | | `options` | `{ policy?, include?, exclude? }` | Row-level policy and field visibility, described below. | ### Grant full access to authenticated users [#grant-full-access-to-authenticated-users] ```typescript title="rayfin/data/Category.ts" import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @authenticated('*') export class Category { @uuid() id!: string; @text({ max: 100 }) name!: string; } ``` ### Public read, authenticated write [#public-read-authenticated-write] Combine `@anonymous()` and `@authenticated()` on the same entity to give each role a different action set: ```typescript title="rayfin/data/Todo.ts" import { entity, anonymous, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @anonymous('read') @authenticated(['create', 'read', 'update', 'delete'], { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @text({ max: 128 }) user_id!: string; } ``` Anyone can read every `Todo`; only a signed-in user can create, update, or delete rows, and only rows where `user_id` matches their own `claims.sub`. ## Row-level policies with the policy DSL [#row-level-policies-with-the-policy-dsl] Pass a `policy` callback in the options object to restrict *which rows* a role can act on. The callback receives a typed `claims` object and a proxy for the entity's own fields (`item`), and returns a comparison Rayfin compiles into a DAB policy string. ```typescript @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) ``` TypeScript infers the entity's shape from the decorated class, so `item.` is autocompleted and renaming a field is a compile-time error everywhere it's referenced. ### Supported claims [#supported-claims] | Claim | Description | | -------------- | --------------------------------------------- | | `claims.sub` | Subject identifier — the signed-in user's ID. | | `claims.email` | The signed-in user's email address. | | `claims.role` | The signed-in user's role. | ### Operators [#operators] | Operator | Example | | ------------- | ----------------------------- | | `.eq(value)` | `claims.sub.eq(item.user_id)` | | `.neq(value)` | `item.status.neq('archived')` | `value` can be another claim or field reference, or a literal string, number, boolean, or `Date`. ### Combining conditions [#combining-conditions] Combine expressions with `.and()` and `.or()`. Both sides are parenthesized automatically, so grouping is always explicit in the generated policy: ```typescript policy: (claims, item) => claims.sub.eq(item.user_id).and(item.isActive.eq(true)) // (claims.role eq 'admin') or (claims.sub eq item.owner_id) policy: (claims, item) => claims.role.eq('admin').or(claims.sub.eq(item.owner_id)) ``` ## Field-level permissions [#field-level-permissions] Use `include` or `exclude` in the options object to control which fields a role can see or write, per action. ```typescript title="rayfin/data/Document.ts" import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @authenticated('read', { policy: (claims, item) => claims.sub.eq(item.owner_id), exclude: ['secret'], }) @authenticated(['create', 'update', 'delete'], { policy: (claims, item) => claims.sub.eq(item.owner_id), }) export class Document { @uuid() id!: string; @text({ max: 128 }) owner_id!: string; @text({ max: 200 }) title!: string; @text({ optional: true, max: 5000 }) secret?: string; } ``` `exclude` hides `secret` from read responses while leaving it writable on create and update. `include` works the other way — list only the fields a role is allowed to touch for that action, useful for a restricted create form: ```typescript @authenticated('create', { policy: (claims, item) => claims.sub.eq(item.createdBy), include: ['title'], }) ``` `include` and `exclude` arrays are typed against the entity's actual property names, so a typo or a renamed field is caught at compile time. ## Action-specific permissions [#action-specific-permissions] Apply multiple decorators with a single action each when the policy or field visibility differs per action, as in the `Document` example above. Rayfin aggregates every decorator on the same class per role; conflicting rules for the same role and action produce a warning when the schema is generated. ```typescript title="rayfin/data/SecureDocument.ts" import { entity, anonymous, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @anonymous('read') @authenticated('create', { policy: (claims, item) => claims.sub.eq(item.createdBy), include: ['title'], }) @authenticated('read', { policy: (claims, item) => claims.sub.eq(item.createdBy), }) @authenticated('update', { policy: (claims, item) => claims.sub.eq(item.createdBy), exclude: ['adminContent'], }) export class SecureDocument { @uuid() id!: string; @text({ max: 200 }) title!: string; @text({ optional: true, max: 5000 }) adminContent?: string; @text({ max: 128 }) createdBy!: string; } ``` ## Storage permissions [#storage-permissions] The same decorators secure storage folders declared with `@blob()` — see [Field types](/docs/data/field-types#blob-storage-folders). Applied to a `@blob()` class, Rayfin generates a storage policy instead of a database policy, using the same `policy` / `include` / `exclude` options: ```typescript title="rayfin/storage/ProfileImage.ts" import { blob, authenticated } from '@microsoft/rayfin-core'; @blob('avatars') @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.owner_id), }) export class ProfileImage { owner_id!: string; } ``` ## Multi-tenant patterns [#multi-tenant-patterns] Row-level policies compare claims to fields on the *same row* — there is no join in the policy itself, and only `claims.sub`, `claims.email`, and `claims.role` are available (see [Supported claims](#supported-claims) above). That covers per-user scoping directly, but scoping rows to a shared organization or team needs a stored `organization_id` plus a membership entity, since there is no organization or tenant claim to compare against. ### Per-user scoping [#per-user-scoping] Per-user scoping is the `policy: (claims, item) => claims.sub.eq(item.user_id)` pattern used throughout this page — every row carries a `user_id` set from the caller's session, and the policy compares it to `claims.sub`. See [Build a todo app](/docs/recipes/todo-app) for this pattern in a complete, deployed app. ### Per-organization scoping [#per-organization-scoping] Rows shared across a team need a stored `organization_id`, plus a membership record that says who belongs to which organization: ```typescript title="rayfin/data/Organization.ts" import { entity, role, uuid, text } from '@microsoft/rayfin-core'; @entity() @role('authenticated', ['read', 'update', 'delete'], { policy: (claims, item) => claims.sub.eq(item.owner_id), }) @role('authenticated', 'create') export class Organization { @uuid() id!: string; @text({ max: 200 }) name!: string; @text({ max: 128 }) owner_id!: string; } ``` ```typescript title="rayfin/data/OrganizationMember.ts" import { entity, role, uuid, text, one } from '@microsoft/rayfin-core'; import { Organization } from './Organization.js'; @entity() @role('authenticated', '*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class OrganizationMember { @uuid() id!: string; @uuid() organization_id!: string; @one(() => Organization) organization?: Organization; @text({ max: 128 }) user_id!: string; } ``` ```typescript title="rayfin/data/Project.ts" import { entity, role, uuid, text, one } from '@microsoft/rayfin-core'; import { Organization } from './Organization.js'; @entity() @role('authenticated', '*', { policy: (claims, item) => claims.sub.eq(item.created_by), }) export class Project { @uuid() id!: string; @text({ max: 200 }) name!: string; @uuid() organization_id!: string; @one(() => Organization) organization?: Organization; @text({ max: 128 }) created_by!: string; } ``` `organization_id` is `@uuid()` here — unlike `user_id`, it *is* a foreign key: it references `Organization.id` through `@one(() => Organization)`, so its type must match the primary key it points at. Import `Organization` with a regular `import` (not `import type`) — the decorator needs the runtime class value, not just its type. The `@role` policy on `Project` above still only enforces ownership by the row's creator (`claims.sub.eq(item.created_by)`) — the policy DSL can't express "the caller is a member of `item.organization_id`" directly, since that requires a join against `OrganizationMember`. Two things layer on top of the row policy to get organization-wide sharing: 1. **Scope every read by organization membership**, looked up first: ```typescript const memberships = await client.data.OrganizationMember.select(['organization_id']) .where({ user_id: { eq: session.user.id } }) .execute(); const projects = await client.data.Project.select(['id', 'name', 'organization_id']) .where({ organization_id: { eq: memberships[0].organization_id } }) .execute(); ``` 2. **Check membership in application code before writes** — verify the caller has an `OrganizationMember` row for the target `organization_id` before creating or updating a `Project` in that organization. The row-level policy alone won't stop a member of one organization from writing into another's `organization_id` if your application code doesn't check first. Layer an admin bypass onto either pattern the same way described in [Combining conditions](#combining-conditions) above — for example, `claims.role.eq('admin').or(claims.sub.eq(item.created_by))` on `Project`. ## Best practices [#best-practices] * Add an explicit permission decorator to every entity — never rely on the default (see the warning at the top of this page). * Include a `user_id` (or similarly named) field on any entity scoped to the signed-in user, and pair it with a `policy` comparing it to `claims.sub`. * Start restrictive and expand as needed — it's easier to widen a policy later than to discover data was over-exposed. * Use separate `@role()` / `@authenticated()` entries per action when field visibility differs by action, as shown above. * Prefer the `@anonymous()` / `@authenticated()` shorthands over `@role('anonymous', ...)` / `@role('authenticated', ...)`. ```prompt title="Add row-level security to an entity" In my Rayfin project, add row-level security to the Todo entity in rayfin/data/Todo.ts so each signed-in user can only read and write their own rows. Use @authenticated('*') with a policy comparing claims.sub to item.user_id. Remove any implicit reliance on default permissions — the decorator must be explicit. Then apply the schema with `rayfin up`. ``` --- --- title: "Querying" description: "Read Rayfin entities with the type-safe select/where/orderBy/execute chain, including filtering, sorting, and cursor pagination." url: https://rayfin.ai/docs/data/querying markdown_url: https://rayfin.ai/docs/data/querying.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: data/querying.mdx --- # Querying > Read Rayfin entities with the type-safe select/where/orderBy/execute chain, including filtering, sorting, and cursor pagination. `RayfinClient` exposes a fluent, typed query builder for every entity in your schema at `client.data.`. It compiles to GraphQL against Data API Builder — you never write a query string by hand. ## Set up the client [#set-up-the-client] Every example below uses the shared `rayfinClient` — see [Set up the client](/docs/data#set-up-the-client) for how to construct it once and reuse it. ## The query chain [#the-query-chain] Build a query by chaining `.select()`, then optionally `.where()` and `.orderBy()`, then call `.execute()`: ```typescript const todos = await rayfinClient.data.Todo.select([ 'id', 'title', 'isCompleted', 'createdAt', ]) .where({ isCompleted: { eq: false } }) .orderBy({ createdAt: 'desc' }) .execute(); ``` `.select()` is required — list every field your code needs. `.where()` and `.orderBy()` are optional and can be omitted or reordered relative to each other, but `.execute()` (or `.executePaginated()`, see [Pagination](#paginate-large-lists)) always comes last. > [!WARNING] > `.execute()` returns a single page — 100 records by default — and does not tell you > whether more records exist. A list that grows past one page is silently truncated. Use > `.execute()` only for queries you know are bounded (a lookup table, a filter that can > match only a handful of rows). For anything unbounded — a user's notes, an order > history — use [pagination](#paginate-large-lists) instead. ## Fetch a single record [#fetch-a-single-record] ```typescript const todo = await rayfinClient.data.Todo.findById('00000000-0000-0000-0000-000000000000'); ``` Use `findById` — not `findByPk`. It returns the record or `null` if no row matches. `findFirst` returns the first record matching an optional filter, or `null`: ```typescript const notebook = await rayfinClient.data.Notebook.findFirst({ name: { eq: 'Work' } }); ``` `findMany` runs a filtered query in one call without building a chain, equivalent to `.select([...]).where(filter).execute()` for cases where you want every field: ```typescript const active = await rayfinClient.data.Todo.findMany({ isCompleted: { eq: false } }); ``` ## Filter with `.where()` [#filter-with-where] `.where()` takes an object keyed by field name. A bare value is shorthand for `eq`; an operator object is more explicit and required for anything other than equality. ```typescript .where({ isCompleted: { eq: true } }) ``` ### Operators by field type [#operators-by-field-type] | Field type | Available operators | | ----------------------- | ---------------------------------------------------------------------------------------------------------- | | `@text()` / `@email()` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `notContains`, `startsWith`, `endsWith`, `isNull`, `in` | | `@int()` / `@decimal()` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `isNull`, `in` | | `@boolean()` | `eq`, `neq`, `isNull`, `in` | | `@date()` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `isNull`, `in` | ### Combine conditions [#combine-conditions] Multiple keys in one `.where()` object are implicitly ANDed. Use explicit `and` / `or` arrays to combine or nest conditions: ```typescript .where({ or: [ { title: { contains: 'urgent' } }, { isCompleted: { eq: false } }, ], }) ``` ### Filter by foreign key, not by dot-path [#filter-by-foreign-key-not-by-dot-path] Filter relationships by their `{property}_id` foreign key column, not by a dot-path into the related entity: ```typescript .where({ notebook_id: { eq: notebookId } }) // correct .where({ 'notebook.id': { eq: notebookId } }) // wrong — dot-paths are select-only ``` For an optional relationship, filter for rows with no related record using `isNull`: ```typescript .where({ notebook: { isNull: true } }) ``` ## Select nested fields with dot-paths [#select-nested-fields-with-dot-paths] Dot-paths are for `.select()` only — use them to pull fields off a related entity into the same result row: ```typescript const notes = await rayfinClient.data.Note.select([ 'id', 'title', 'notebook_id', 'notebook.id', 'notebook.name', ]) .orderBy({ createdAt: 'desc' }) .execute(); // notes[0].notebook.name is available directly ``` Nesting is supported to exactly one level past the root entity — `notebook.name` works, but `notebook.owner.email` (a third level) does not. ## Sort with `.orderBy()` [#sort-with-orderby] Sort directions are the lowercase strings `'asc'` and `'desc'` — not capitalized constants: ```typescript .orderBy({ createdAt: 'desc' }) ``` ## Paginate large lists [#paginate-large-lists] Because `.execute()` returns only one page and gives no signal that more records exist, use `.first(n)` with `.executePaginated()` for any query that can grow past a single page. ### Fetch one page [#fetch-one-page] ```typescript const page = await rayfinClient.data.Note.select(['id', 'title', 'createdAt']) .orderBy({ createdAt: 'desc' }) .first(25) .executePaginated(); page.items; // up to 25 records page.hasNextPage; // true if more records remain page.endCursor; // pass to .after() to fetch the next page ``` ### Fetch the next page [#fetch-the-next-page] ```typescript const nextPage = await rayfinClient.data.Note.select(['id', 'title', 'createdAt']) .orderBy({ createdAt: 'desc' }) .first(25) .after(page.endCursor) .executePaginated(); ``` Keep `.select()`, `.where()`, and `.orderBy()` identical across every page in a sequence — a stable sort order is required for the cursor to advance correctly. ### Fetch every record [#fetch-every-record] Loop, passing each page's `endCursor` into the next call's `.after()`, until `hasNextPage` is `false`: ```typescript async function fetchAllNotes() { const all: Array<{ id: string; title: string; createdAt: Date }> = []; let cursor: string | undefined; do { const page = await rayfinClient.data.Note.select(['id', 'title', 'createdAt']) .orderBy({ createdAt: 'desc' }) .first(100) .after(cursor) .executePaginated(); all.push(...page.items); cursor = page.hasNextPage ? page.endCursor : undefined; } while (cursor); return all; } ``` > [!NOTE] > `.first(n)` is bounded by Data API Builder's maximum page size of 100,000. > `.first(-1)` requests an unbounded page and still hits that same cap, so it only works > when the full result set fits under it. For anything that might not, page through > results with `.after()` instead of requesting everything in one large `.first(n)`. > > `PagedResult` also exposes a `totalCount` field, but Data API Builder does not populate > it on paginated queries — do not rely on it. ## Counting records [#counting-records] There is no `count()` on the fluent query chain. What exists is [aggregation](/docs/data/aggregations) — `count` is one of five operations available through `groupBy()` and `aggregate()`: ```typescript const [totals] = await rayfinClient.data.Order .where({ status: { eq: 'open' } }) .aggregate({ open: { count: 'amount' } }) .execute(); totals.aggregations.open; // number ``` > [!IMPORTANT] > `count` aggregates **numeric values**, not rows. Data API Builder types every > aggregation's field argument as the entity's numeric fields, so you can only count a > numeric column — and the number it returns is the count of rows where that column is > non-null. That makes `count` a true row count only when you point it at a non-nullable numeric column. For an entity that has none — a `Todo` with a `uuid` id and `text` title, say — fall back to counting client-side: ```typescript const openTodos = await rayfinClient.data.Todo.select(['id']) .where({ isCompleted: { eq: false } }) .execute(); const openCount = openTodos.length; // only correct if the result fits in one page ``` For a count that might exceed one page, page through with `.executePaginated()` and sum `page.items.length` across pages, since `.execute()` truncates and `totalCount` is not populated. ```prompt title="Add a paginated list query" In my Rayfin project, write a function that fetches all Todo records for the current user's "Archive" view using the RayfinClient. Select id, title, isCompleted, and createdAt, filter to isCompleted: { eq: true }, order by createdAt descending, and page through with .first(50) and .executePaginated() until hasNextPage is false, returning the combined list. ``` --- --- title: "Relationships" description: "Model one-to-many associations between Rayfin entities with @one and @many, and work around the lack of native many-to-many support." url: https://rayfin.ai/docs/data/relationships markdown_url: https://rayfin.ai/docs/data/relationships.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: data/relationships.mdx --- # Relationships > Model one-to-many associations between Rayfin entities with @one and @many, and work around the lack of native many-to-many support. Rayfin supports one-to-many (and its inverse, many-to-one) relationships between entities using the `@one()` and `@many()` navigation decorators. Rayfin generates the foreign key column for you — you describe the relationship, not the join. Many-to-many relationships are not supported natively. This page covers both the supported one-to-many pattern and the explicit join-entity workaround for many-to-many. ## `@one()` and `@many()` [#one-and-many] * `@one(() => Target)` on the "many" side declares a many-to-one reference to a single related record. Rayfin auto-generates a `{property}_id` foreign key column for it. * `@many(() => Target)` on the "one" side declares the inverse: a collection of related records. It does not generate a column — it is a read-only navigation back to the rows whose `@one()` field points at this record. ```typescript title="rayfin/data/Notebook.ts" import { entity, authenticated, uuid, text, boolean, date, many } from '@microsoft/rayfin-core'; import { Note } from './Note.js'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Notebook { @uuid() id!: string; @text({ max: 100 }) name!: string; @boolean({ default: false }) isDefault!: boolean; @date() createdAt!: Date; @many(() => Note) notes?: Note[]; @text({ max: 128 }) user_id!: string; } ``` ```typescript title="rayfin/data/Note.ts" import { entity, authenticated, uuid, text, date, one } from '@microsoft/rayfin-core'; import { Notebook } from './Notebook.js'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Note { @uuid() id!: string; @text({ max: 200 }) title!: string; @text({ max: 10000 }) content!: string; @date() createdAt!: Date; @uuid() notebook_id!: string; @one(() => Notebook, { optional: true }) notebook?: Notebook; @text({ max: 128 }) user_id!: string; } ``` Register both in `rayfin/data/schema.ts` — see [Modeling entities](/docs/data/modeling#register-the-entity-in-schemats). ## Lazy arrow functions [#lazy-arrow-functions] `@one()` and `@many()` both take a **function that returns the target class** (`() => Notebook`), not the class itself. This lazy form lets two entity files reference each other — `Notebook` references `Note` and `Note` references `Notebook` — without a circular `import` failing at module-load time. The function is only called after both modules have finished loading. ```typescript @many(() => Note) notes?: Note[]; // correct — lazy reference @many(Note) notes?: Note[]; // wrong — evaluated immediately, breaks on circular imports ``` ## Use `import`, not `import type` [#use-import-not-import-type] Import the target entity class with a plain `import`, never `import type`: ```typescript import { Notebook } from './Notebook.js'; // correct — decorators need the runtime class import type { Notebook } from './Notebook.js'; // wrong — erased at compile time, decorator has nothing to call ``` `@one()` and `@many()` store the arrow function and call it at runtime to resolve the target entity's metadata. `import type` is erased entirely by the TypeScript compiler, so the arrow function would close over a name that no longer exists at runtime. ## Foreign key columns [#foreign-key-columns] Rayfin auto-generates the foreign key column when you declare `@one()` — you do not need to define it yourself. Define the FK field explicitly only when your application code needs to read or set it directly (for example, filtering by it — see [Querying](/docs/data/querying#filter-by-foreign-key-not-by-dot-path)). * **Naming**: when you do define it, the field must follow the `{property}_id` convention — `notebook_id` for a `notebook` navigation property. Custom key names are not supported; `foreignKey` and `targetKey` options do not exist on `@one()` / `@many()`. * **Type**: a foreign key field referencing another entity's primary key must be declared `@uuid()`, matching the type of that entity's `id`. Declaring it `@text()` is a type mismatch with the column it references. * **Auth-derived owner columns are the exception.** A `user_id` field populated from `claims.sub` (the signed-in user's subject claim) is **not** a foreign key to another Rayfin entity — it is a plain `@text()` field, as shown in the `Notebook` and `Note` examples above. ```typescript @uuid() notebook_id!: string; // correct — FK to Notebook.id (a uuid) @text() notebook_id!: string; // wrong — type mismatch with the referenced uuid PK @text({ max: 128 }) user_id!: string; // correct — claims.sub is not a Rayfin entity FK ``` ## Option limits on relationship decorators [#option-limits-on-relationship-decorators] `@one()` and `@many()` accept only `{ optional?: boolean, unique?: boolean }` as their second argument — no `default`, `max`, or the other field options described in [Field types](/docs/data/field-types). Mark a `@one()` relationship `{ optional: true }` when the related record may not exist, matching a `?` on the property, the same nullable pattern used for scalar fields. ```typescript @one(() => Notebook, { optional: true }) notebook?: Notebook; // a note may be unfiled @one(() => Notebook) notebook!: Notebook; // every note must have one ``` ## Many-to-many: use an explicit join entity [#many-to-many-use-an-explicit-join-entity] Rayfin does not support many-to-many relationships directly. Model them the same way you would in raw SQL: an explicit join entity with two `@one()` fields, one pointing at each side of the relationship. ```typescript title="rayfin/data/Tag.ts" import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @authenticated('*') export class Tag { @uuid() id!: string; @text({ max: 50, unique: true }) name!: string; } ``` ```typescript title="rayfin/data/TodoTag.ts" import { entity, authenticated, uuid, one } from '@microsoft/rayfin-core'; import { Todo } from './Todo.js'; import { Tag } from './Tag.js'; @entity() @authenticated('*') export class TodoTag { @uuid() id!: string; @uuid() todo_id!: string; @one(() => Todo) todo!: Todo; @uuid() tag_id!: string; @one(() => Tag) tag!: Tag; } ``` Query through the join entity rather than expecting a direct `tags` collection on `Todo`: select `TodoTag` rows filtered by `todo_id`, with `tag.name` in the field selection (see [dot-path selection](/docs/data/querying#select-nested-fields-with-dot-paths)). ```typescript const todoTags = await client.data.TodoTag.select(['id', 'tag.id', 'tag.name']) .where({ todo_id: { eq: todoId } }) .execute(); ``` ```prompt title="Model a many-to-many relationship" In my Rayfin project, add tagging support to the Todo entity. Create a new Tag entity in rayfin/data/Tag.ts with a unique "name" field (text, max 50). Create a join entity TodoTag in rayfin/data/TodoTag.ts with a uuid id, a todo_id foreign key with a @one() reference to Todo, and a tag_id foreign key with a @one() reference to Tag. Register both new entities in rayfin/data/schema.ts, then apply the schema with `rayfin up`. ``` --- --- title: "Seeding data" description: "Populate @anonymous() Rayfin entities with RayfinServerClient in a Node.js script — @authenticated() entities have no scripted seeding path today." url: https://rayfin.ai/docs/data/seeding markdown_url: https://rayfin.ai/docs/data/seeding.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: data/seeding.mdx --- # Seeding data > Populate @anonymous() Rayfin entities with RayfinServerClient in a Node.js script — @authenticated() entities have no scripted seeding path today. Use `RayfinServerClient` from a plain Node.js script to populate development data — the same `client.data.` calls you use from your frontend, just run with `tsx` instead of in a browser. A script has no browser, so it can only authenticate with the `publishableKey` your project already generates — enough to satisfy an `@anonymous()` grant, and nothing else. See [No supported path for `@authenticated()` entities](#no-supported-path-for-authenticated-entities) below if the entity you want to seed requires a signed-in user. ## What a seed script can authenticate as [#what-a-seed-script-can-authenticate-as] `RayfinServerClient` requires `publishableKey`; `accessToken` is optional: ```typescript new RayfinServerClient({ baseUrl: env['RAYFIN_PUBLIC_API_URL'], publishableKey: env['RAYFIN_PUBLIC_PUBLISHABLE_KEY'], }); ``` Use `RayfinServerClient`, not `RayfinClient` — it skips the browser-coupled `Auth` module entirely. See [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client#rayfinserverclient). Without an `accessToken`, every request carries only the publishable key. That satisfies an `@anonymous()` grant and nothing else — it cannot satisfy `@authenticated()`, which requires a valid signed-in user's access token on every request (see [Built-in roles](/docs/data/permissions#built-in-roles)). There is no supported way for a Node.js script to acquire that token itself. Fabric SSO signs a user in through a browser popup or the Fabric portal iframe (see [Fabric SSO](/docs/auth/fabric-sso)) — flows a headless script cannot drive — and, per [Sessions](/docs/auth/sessions#token-handling), Rayfin doesn't expose a way to read a token back out for reuse elsewhere. `accessToken` on `RayfinServerClient` exists for code that already holds a token issued by some other trusted flow — for example, a Rayfin function forwarding the caller's own request token — not as a way to sign a script in. Service principal credentials (`rayfin login --service-principal`) don't help here either — they authenticate the CLI itself for deployment (`rayfin up`, `rayfin up db apply`), a separate concern from a signed-in user's access token that `client.data.*` calls check. ## Seeding an anonymous entity [#seeding-an-anonymous-entity] Reference or lookup data with no owner — content anyone can read, that only you create — is the case a seed script handles cleanly, because it needs no caller identity at all: ```typescript title="rayfin/data/FaqCategory.ts" import { entity, anonymous, uuid, text } from '@microsoft/rayfin-core'; @entity() @anonymous('*') export class FaqCategory { @uuid() id!: string; @text({ max: 100 }) name!: string; } ``` ```typescript title="rayfin/data/FaqEntry.ts" import { entity, anonymous, uuid, text, one } from '@microsoft/rayfin-core'; import { FaqCategory } from './FaqCategory.js'; @entity() @anonymous('*') export class FaqEntry { @uuid() id!: string; @text({ max: 200 }) question!: string; @text({ max: 2000 }) answer!: string; @uuid() category_id!: string; @one(() => FaqCategory) category!: FaqCategory; } ``` > [!NOTE] > A tenant admin has to enable "Enable anonymous data access for Fabric Apps" before > Fabric grants the `anonymous` role at all — see > [Built-in roles](/docs/data/permissions#built-in-roles). Without it, even a correctly > decorated `@anonymous()` entity rejects anonymous requests. Read the backend URL and publishable key from `rayfin/.env` (generated by `rayfin up`) rather than hardcoding them — the port and key vary per project and per deployment: ```typescript title="scripts/seed.ts" import { readFileSync } from 'fs'; import { RayfinServerClient } from '@microsoft/rayfin-client'; import type { AppSchema } from '../rayfin/data/schema'; function loadEnv(): Record { const vars: Record = {}; for (const line of readFileSync('rayfin/.env', 'utf-8').split('\n')) { const match = line.match(/^([^#=]+)=(.+)$/); if (match) vars[match[1].trim()] = match[2].trim(); } return vars; } const env = loadEnv(); const client = new RayfinServerClient({ baseUrl: env['RAYFIN_PUBLIC_API_URL'], publishableKey: env['RAYFIN_PUBLIC_PUBLISHABLE_KEY'], }); async function seed() { // Create parent records first, then children that reference their IDs. const category = await client.data.FaqCategory.create({ name: 'Billing' }); await client.data.FaqEntry.create({ question: 'How do I change my plan?', answer: 'Open Settings → Billing and choose a new plan.', category: { id: category.id }, }); } seed().catch(console.error); ``` Run it with `tsx`: ```bash npx tsx scripts/seed.ts ``` Or wire it into `package.json` for a shorter command: ```json title="package.json" { "scripts": { "seed": "tsx scripts/seed.ts" } } ``` ```bash npm run seed ``` **Create parents before children.** A `@one()` relationship needs the parent's `id` to exist first — create the `FaqCategory` before the `FaqEntry` that references it, as shown above. See [Relationships](/docs/data/relationships) for how `@one()` / `@many()` work. ### Seeding a single entity [#seeding-a-single-entity] For an entity with no relationships, the script is shorter — just create: ```typescript title="scripts/seed-faq-categories.ts" import { readFileSync } from 'fs'; import { RayfinServerClient } from '@microsoft/rayfin-client'; import type { AppSchema } from '../rayfin/data/schema'; function loadEnv(): Record { const vars: Record = {}; for (const line of readFileSync('rayfin/.env', 'utf-8').split('\n')) { const match = line.match(/^([^#=]+)=(.+)$/); if (match) vars[match[1].trim()] = match[2].trim(); } return vars; } const env = loadEnv(); const client = new RayfinServerClient({ baseUrl: env['RAYFIN_PUBLIC_API_URL'], publishableKey: env['RAYFIN_PUBLIC_PUBLISHABLE_KEY'], }); async function seed() { const names = ['Billing', 'Account', 'Troubleshooting']; for (const name of names) { await client.data.FaqCategory.create({ name }); } } seed().catch(console.error); ``` ## No supported path for `@authenticated()` entities [#no-supported-path-for-authenticated-entities] Most application data is not anonymous — anything with a `user_id`/`owner_id` and a row-level policy, like the `Todo` entity in [Build a todo app](/docs/recipes/todo-app), is scoped with `@authenticated()`. As established above, a seed script cannot obtain a token for any user, so **there is no supported way today to script seed data into an entity that requires authentication.** If you need rows in an entity like that for development or testing, create them the way a real signed-in user would — through the running app itself, signed in with Fabric SSO — rather than through a script. Do not hand-roll an `Authorization` header or fabricate a token shaped like a Fabric-issued JWT: there is no supported way to mint a valid one outside Fabric SSO, and Rayfin rejects anything that doesn't verify against its own signing keys. ```prompt title="Write a seed script for anonymous reference data" In my Rayfin project, I have an FaqCategory entity (id, name) and an FaqEntry entity (id, question, answer, category_id, with a @one(() => FaqCategory) relationship named category), both decorated @anonymous('*') as public reference data with no owner. Write a Node.js seed script at scripts/seed.ts using RayfinServerClient from @microsoft/rayfin-client. Read RAYFIN_PUBLIC_API_URL and RAYFIN_PUBLIC_PUBLISHABLE_KEY from rayfin/.env — do not pass an accessToken, since this client only needs to satisfy the anonymous role. Create one FaqCategory named "Billing", then one FaqEntry in that category. Add a "seed" script to package.json that runs it with tsx. ``` --- --- title: "Validation" description: "Generate a Standard Schema validator directly from a Rayfin entity to validate form input without a separate validation library." url: https://rayfin.ai/docs/data/validation markdown_url: https://rayfin.ai/docs/data/validation.md section: data product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: data/validation.mdx --- # Validation > Generate a Standard Schema validator directly from a Rayfin entity to validate form input without a separate validation library. Rayfin can build a [Standard Schema](https://standardschema.dev) validator directly from a decorated entity class. Field constraints — required, max length, numeric range, enum membership — come from the same `@text()`, `@int()`, `@set()`, and other decorators you already wrote for the database schema, so there is nothing to duplicate and nothing to add: no separate Zod or Yup schema to keep in sync. > [!NOTE] > Rayfin entities use TC39 Stage 3 decorators, so any tool compiling your client code > must target **ES2022** or later. If you see `Expression expected` on `@entity()` (or > another decorator) when calling `toStandardSchema` or `getFieldConstraints`, your > bundler's target is too old. For Vite, set `target: 'es2022'` in `vite.config.ts` and > keep the default `@vitejs/plugin-react` (esbuild) plugin — `@vitejs/plugin-react-swc` > cannot parse these decorators regardless of `target`. ## Build a validator from an entity [#build-a-validator-from-an-entity] Call `toStandardSchema` with the entity class. The primary key (`id`) is always omitted automatically — pass any other server-managed fields (timestamps, an owner ID your code sets from the session) in the `omit` option. ```typescript title="src/forms/todoSchema.ts" import { toStandardSchema } from '@microsoft/rayfin-core'; import { Todo } from '../../rayfin/data/Todo.js'; // id is auto-omitted. List additional fields the form does not collect. export const todoInputSchema = toStandardSchema(Todo, { omit: ['createdAt', 'isCompleted', 'user_id'] as const, }); ``` The returned object implements the Standard Schema v1 contract (`~standard`), so it works with any compatible library — TanStack Form, Conform, tRPC v11, and others. It also exposes a `.validate()` method for direct use without going through that protocol. ## Validate form input [#validate-form-input] Call `.validate()` with the raw form values. The result is `{ value }` on success or `{ issues }` on failure — never both. ```typescript const result = todoInputSchema.validate({ title: title.trim(), }); if (result.issues) { const errors: Record = {}; for (const issue of result.issues) { const key = String(issue.path?.[0] ?? '_'); if (!errors[key]) errors[key] = issue.message; } // errors.title, for example, holds the first message for that field } else { // result.value is typed as Omit await createTodo(result.value.title); } ``` Validation is synchronous — every check (type, length, regex, enum membership) runs in memory with no network or async overhead. Unknown fields not declared on the entity are rejected. ## What gets validated [#what-gets-validated] | Decorator | Checks | | ------------ | --------------------------------------------------------- | | `@text()` | Is a string. Enforces `min`, `max`, and `regex` when set. | | `@uuid()` | Is a string matching the UUID format. | | `@email()` | Is a string matching a practical email pattern. | | `@int()` | Is a finite integer. Enforces `min` and `max`. | | `@decimal()` | Is a finite number. Enforces `min` and `max`. | | `@boolean()` | Is a boolean. | | `@date()` | Is a `Date`, an ISO string, or a numeric timestamp. | | `@set()` | Value is one of the declared literal values. | Required fields (the default) produce a "required" issue when missing or `null`. Optional fields (`{ optional: true }`) are silently skipped when absent. ## Read field constraints for UI hints [#read-field-constraints-for-ui-hints] `getFieldConstraints` reads a single field's decorator constraints without building a full schema — useful for a character counter or a "required" label. ```typescript import { getFieldConstraints } from '@microsoft/rayfin-core'; import { Todo } from '../../rayfin/data/Todo.js'; const titleConstraints = getFieldConstraints(Todo, 'title'); // { type: 'string', min: undefined, max: 200, optional: false } const maxLength = titleConstraints?.type === 'string' ? titleConstraints.max : undefined; ``` The field name is checked against the entity's actual properties, so a typo is a compile-time error, not a runtime surprise. ## Auto-omit behavior [#auto-omit-behavior] `toStandardSchema` automatically excludes: * The `id` primary key — server-generated, never a form input. * Relationship navigation properties (`@one`, `@many`) — these are set on the mutation call directly (see [Creating, updating, deleting](/docs/data/mutations)), not collected from a form field. List any other server-managed fields — timestamps, an owner ID taken from the session — in `omit`. The array is type-checked against the entity, so a misspelled field name fails to compile. ## Complete React example [#complete-react-example] ```tsx title="src/forms/TodoForm.tsx" import { useMemo, useState, type FormEvent } from 'react'; import { toStandardSchema, getFieldConstraints } from '@microsoft/rayfin-core'; import { Todo } from '../../rayfin/data/Todo.js'; interface TodoFormProps { onSubmit: (value: { title: string }) => Promise; } export function TodoForm({ onSubmit }: TodoFormProps) { const [title, setTitle] = useState(''); const [error, setError] = useState(''); const todoInputSchema = useMemo( () => toStandardSchema(Todo, { omit: ['createdAt', 'isCompleted', 'user_id'] as const, }), [], ); const titleConstraints = getFieldConstraints(Todo, 'title'); const maxLength = titleConstraints?.type === 'string' ? titleConstraints.max : undefined; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); const result = todoInputSchema.validate({ title: title.trim() }); if (result.issues) { setError(result.issues[0].message); return; } setError(''); await onSubmit(result.value); setTitle(''); }; return (
setTitle(e.target.value)} /> {maxLength && ( {title.length}/{maxLength} )} {error &&

{error}

}
); } ``` ## Standard Schema interop [#standard-schema-interop] The object returned by `toStandardSchema` implements `StandardSchemaV1` from `@standard-schema/spec`. Any library that reads the `~standard` property consumes it directly — you do not need `.validate()` in that case: ```typescript // TanStack Form, Conform, tRPC v11, etc. read ~standard automatically. // Access it explicitly only if you need to call it outside such a library: const result = todoInputSchema['~standard'].validate(formValues); ``` `RayfinStandardSchema` and `StandardSchemaV1` are re-exported from `@microsoft/rayfin-core`, so you do not need a direct dependency on `@standard-schema/spec` just to reference the types. ```prompt title="Add validated form input for an entity" In my Rayfin project, build a form validator for the Category entity in rayfin/data/Category.ts using toStandardSchema from @microsoft/rayfin-core. Only the "name" field should be collected from the form (omit any other server-managed fields). Write a small React component that validates on submit, shows the first validation error, and calls an onSubmit prop with the validated value. ``` --- --- title: "Calling functions from your app" description: "Invoke Rayfin functions from the frontend with a type-safe FunctionClient — client.functions..invoke() and error handling." url: https://rayfin.ai/docs/functions/calling-functions markdown_url: https://rayfin.ai/docs/functions/calling-functions.md section: functions product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: functions/calling-functions.mdx --- # Calling functions from your app > Invoke Rayfin functions from the frontend with a type-safe FunctionClient — client.functions..invoke() and error handling. Once a function is registered and its schema is generated (see [Writing a function](/docs/functions/writing-functions)), calling it from the frontend is a single typed call through `RayfinClient` — no separate HTTP client or manual request shaping. > [!WARNING] > Functions are experimental and are not available in every Fabric region or tenant. See > [Functions](/docs/functions) before you depend on them. ## Give `RayfinClient` your functions schema [#give-rayfinclient-your-functions-schema] Import the generated `AppFunctionsSchema` from your functions project and pass it as `RayfinClient`'s second type parameter, alongside your data schema: ```typescript title="src/services/rayfinClient.ts" import { RayfinClient } from '@microsoft/rayfin-client'; import type { AppFunctionsSchema } from '../../rayfin/functions/src/types.js'; import type { AppSchema } from '../../rayfin/data/schema'; const client = new RayfinClient({ baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, }); ``` `AppFunctionsSchema` is a closed object type — only the function names it lists are accepted by `client.functions..invoke(...)`, and each one's parameter and return types are checked against the schema entry. ## Invoking a function [#invoking-a-function] ```typescript // A function with input: const greeting = await client.functions.helloWorld.invoke({ firstName: 'Ada', lastName: 'Lovelace', }); console.log(greeting); // typed as string // A void-input function (RayfinContext-only handler): const entries = await client.functions.getEntries.invoke(); ``` `invoke()` resolves with the function's output directly — `Promise`, not a wrapper envelope. Pass an options object for extra per-call headers: ```typescript await client.functions.helloWorld.invoke( { firstName: 'Ada', lastName: 'Lovelace' }, { headers: { 'x-request-id': crypto.randomUUID() } } ); ``` ## Handling errors [#handling-errors] `invoke()` throws rather than returning an error value — by the time it resolves, the result is safe to use without checking for `undefined`: ```typescript import { FunctionsError } from '@microsoft/rayfin-functions'; try { const result = await client.functions.helloWorld.invoke({ firstName, lastName }); } catch (error) { if (error instanceof FunctionsError) { console.error('Function invocation failed:', error.message, error.code); } else { // NetworkError (transport-level issues) or SdkError (anything else // unexpected) from @microsoft/rayfin-lib. console.error('Unexpected error calling function:', error); } } ``` * A non-empty `errors` array or a non-success status in the underlying response surfaces as a `FunctionsError`. * Network failures are wrapped in a `NetworkError`; anything else unexpected is wrapped in a base `SdkError`. * The server-side `invocationId` is logged via `console.debug` alongside the function name, so it's available for correlation without being part of the typed return value. Never call `client.functions..invoke()` before functions are deployed — see [Deploying functions](/docs/functions/deploying) to enable `services.functions` and ship your functions project with `rayfin up`. ```prompt title="Call a Rayfin function from the frontend" In my Rayfin app, wire up calling a function from the frontend: - Update the RayfinClient construction in src/services/rayfinClient.ts to pass AppFunctionsSchema (imported from rayfin/functions/src/types.js) as the second type parameter, alongside my existing data schema. - Call client.functions..invoke(...) with the right typed parameters, and handle failures by catching FunctionsError from @microsoft/rayfin-functions specifically before falling back to a generic error handler. Show me the updated client setup and the call site. ``` --- --- title: "Connections" description: "Connect a Rayfin function to external services with delegated auth — AudienceType values, the ctx.getToken() pattern, and SQL/Key Vault/OneLake examples." url: https://rayfin.ai/docs/functions/connections markdown_url: https://rayfin.ai/docs/functions/connections.md section: functions product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: functions/connections.mdx --- # Connections > Connect a Rayfin function to external services with delegated auth — AudienceType values, the ctx.getToken() pattern, and SQL/Key Vault/OneLake examples. Functions reach external resources through **delegated authentication**: the runtime exchanges the calling user's identity token for a resource-scoped on-behalf-of (OBO) token, so your function accesses resources **as the calling user**, not as a shared service identity. You declare a connection on the function, and the runtime hands you a scoped token at invocation time — you never manage a long-lived credential yourself. See [Delegated access](/docs/auth/delegated-access) for the overview of every delegated-auth surface. For the connector equivalent, see [Connector authentication](/docs/connectors/auth). > [!WARNING] > Functions and delegated authentication are experimental and are not available in every > Fabric region or tenant. See [Functions](/docs/functions) before you depend on them. ## The connection pattern [#the-connection-pattern] Declare a connection in the third argument to `udf.func()`, and read the token inside the handler with `ctx.getToken(audienceType)`: ```typescript title="rayfin/functions/src/function_app.ts" import { AudienceType } from '@microsoft/fabric-user-data-functions'; udf.func( 'myFunction', async (ctx: RayfinContext /*, ...user params */): Promise => { const token = ctx.getToken(AudienceType.X); // Use the token with the appropriate SDK client. }, [udf.connection({ audienceType: AudienceType.X })], ); ``` For Azure SDK clients that expect a `TokenCredential` rather than a raw string, wrap it: ```typescript title="rayfin/functions/src/ContextTokenCredential.ts" import type { TokenCredential, AccessToken } from '@azure/identity'; export class ContextTokenCredential implements TokenCredential { constructor(private readonly token: string) {} async getToken(): Promise { return { token: this.token, expiresOnTimestamp: Date.now() + 3600_000 }; } } ``` ## Available `AudienceType` values [#available-audiencetype-values] | `AudienceType` | Resource | Ask the user for | | -------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- | | `CosmosDB` | Azure Cosmos DB | The account endpoint plus database/container names | | `KeyVault` | Azure Key Vault | The vault URL, e.g. `https://my-vault.vault.azure.net/` | | `EventGrid` | Azure Event Grid | The topic endpoint | | `Sql` | Fabric Lakehouse/Warehouse/SQL DB/Mirrored DB, Azure SQL | The SQL analytics endpoint or connection string from the Fabric portal | | `Storage` | OneLake (DFS), Azure Blob/Table/Queue | The file URL from Lakehouse properties, or a storage account URL | | `Fabric` | Fabric platform APIs | The workspace and item IDs the function should act on | | `AzureAI` | Azure AI Foundry | The project endpoint URL and the model deployment name | | `ADO` | Azure DevOps | The organization and project names | | `Kusto` | Azure Data Explorer (Kusto) | The cluster URI and database name | | `WorkIQ` | WorkIQ | The service endpoint | `AzureAI`, `Kusto`, `ADO`, and `WorkIQ` are audiences the Fabric host does not yet resolve natively, so the SDK supplies the OBO scope for them. They work the same way from your code — declare the connection and call `ctx.getToken()`. ## SQL connections [#sql-connections] All Fabric SQL resources (Lakehouse SQL analytics, Warehouse, SQL Database, Mirrored Database) share the same requirements: * **Package:** `mssql@^12.6.0` (which pulls in `tedious >= 19.2.2`). Older `tedious` (`<= 19.1.2`) has a LOGIN7 FeatureExt bug that causes "socket hang up" errors on Fabric endpoints. * **Encryption:** `encrypt: true` — not `'strict'`. This matches ODBC's `Encrypt=yes`. * **Auth:** `azure-active-directory-access-token`, using `ctx.getToken(AudienceType.Sql)`. | Resource | What to ask for | `database` value | | ------------------------- | ------------------------------------------------------ | ------------------------------------------ | | Lakehouse (SQL analytics) | SQL analytics endpoint + item GUID (Portal → Settings) | Item GUID (Initial Catalog) — **required** | | Warehouse | SQL endpoint + item GUID (Portal → Settings) | Item GUID | | SQL Database | Full connection string (Portal → Connection strings) | Database name from the connection string | | Mirrored Database | SQL analytics endpoint + item GUID (Portal → Settings) | Item GUID | > [!WARNING] > For Lakehouse, Warehouse, and Mirrored Database, you **must** pass the item GUID as > `database`. Without it, multi-item workspaces can't route the connection correctly. ```typescript title="rayfin/functions/src/function_app.ts" import sql from 'mssql'; import { AudienceType } from '@microsoft/fabric-user-data-functions'; // Always ask the user for these values — never invent them. const SQL_SERVER = '.datawarehouse.fabric.microsoft.com'; const DATABASE = ''; udf.func( 'queryData', async (ctx: RayfinContext, query: string): Promise[]> => { const token = ctx.getToken(AudienceType.Sql); const pool = await sql.connect({ server: SQL_SERVER, database: DATABASE, options: { encrypt: true, trustServerCertificate: false }, authentication: { type: 'azure-active-directory-access-token', options: { token } }, }); const result = await pool.request().query(query); await pool.close(); return result.recordset; }, [udf.connection({ audienceType: AudienceType.Sql })], ); ``` ## OneLake files (DFS) [#onelake-files-dfs] Ask the user for the file URL: Fabric portal → Lakehouse → file → Properties → URL. It has the shape `https://onelake.dfs.fabric.microsoft.com///Files/`. ```typescript title="rayfin/functions/src/function_app.ts" import { AudienceType } from '@microsoft/fabric-user-data-functions'; udf.func( 'readFile', async (ctx: RayfinContext, fileUrl: string): Promise => { const token = ctx.getToken(AudienceType.Storage); const res = await fetch(fileUrl, { headers: { Authorization: `Bearer ${token}` } }); if (!res.ok) throw new Error(`OneLake read failed: ${res.status}`); return res.text(); }, [udf.connection({ audienceType: AudienceType.Storage })], ); ``` ## Azure Key Vault [#azure-key-vault] Ask the user for the vault URL (e.g. `https://my-vault.vault.azure.net/`). Install `@azure/keyvault-secrets` and `@azure/identity` in `rayfin/functions/`: ```typescript title="rayfin/functions/src/function_app.ts" import { SecretClient } from '@azure/keyvault-secrets'; import { AudienceType } from '@microsoft/fabric-user-data-functions'; import { ContextTokenCredential } from './ContextTokenCredential.js'; udf.func( 'getSecret', async (ctx: RayfinContext, kvUrl: string, secretName: string): Promise => { const credential = new ContextTokenCredential(ctx.getToken(AudienceType.KeyVault)); const client = new SecretClient(kvUrl, credential); const secret = await client.getSecret(secretName); return secret.value ?? ''; }, [udf.connection({ audienceType: AudienceType.KeyVault })], ); ``` ## Other resources [#other-resources] Cosmos DB and Blob Storage follow the same `ContextTokenCredential` pattern — ask the user for the resource endpoint, wrap `ctx.getToken(AudienceType.X)`, and pass the credential to the relevant Azure SDK client: * **Cosmos DB** (`AudienceType.CosmosDB`) — `new CosmosClient({ endpoint, aadCredentials: credential })`, from `@azure/cosmos`. * **Blob Storage** (`AudienceType.Storage`) — `new BlobServiceClient(url, credential)`, from `@azure/storage-blob`. ## Rules [#rules] * Always ask the user for real endpoint URLs — never invent them, and never fall back to `process.env` as your primary source for a resource endpoint. * Declare connections in the third argument to `udf.func()`. * Use `ctx.getToken(AudienceType.X)` — never acquire tokens manually. * Install SDK packages (`mssql`, `@azure/identity`, etc.) in `rayfin/functions/package.json`, not the project root. * For SQL, use `mssql@^12.6.0` with `encrypt: true`. ```prompt title="Add a connection to a Rayfin function" In my Rayfin project's rayfin/functions/src/function_app.ts, add a function that connects to an external resource (tell me which one — SQL, Key Vault, OneLake, Cosmos DB, or Blob Storage) using a delegated-auth connection. Ask me for the real endpoint URL, connection string, or vault URL rather than inventing one. Declare the connection with udf.connection({ audienceType: AudienceType. }) in the third argument to udf.func, and get the token inside the handler with ctx.getToken(AudienceType.) — do not acquire tokens any other way. If the target is a SQL resource, use the mssql package (^12.6.0) with encrypt: true and azure-active-directory-access-token auth, and make sure I've given you the item GUID to use as the database value. Install any needed SDK packages in rayfin/functions/package.json, not the project root. ``` --- --- title: "Deploying functions" description: "How Rayfin functions ship to Fabric with rayfin up, and how to deploy just the functions project with rayfin up functions deploy." url: https://rayfin.ai/docs/functions/deploying markdown_url: https://rayfin.ai/docs/functions/deploying.md section: functions product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: functions/deploying.mdx --- # Deploying functions > How Rayfin functions ship to Fabric with rayfin up, and how to deploy just the functions project with rayfin up functions deploy. > [!WARNING] > Functions are experimental and are not available in every Fabric region or tenant. The > deployment behavior below is the current documented behavior and may change. See > [Functions](/docs/functions). ## Enable functions before deploying [#enable-functions-before-deploying] Functions deploy only when the service is turned on in `rayfin/rayfin.yml`: ```yaml title="rayfin/rayfin.yml" services: functions: enabled: true ``` See [Functions](/docs/functions) for enabling the service and [Writing a function](/docs/functions/writing-functions) for scaffolding `rayfin/functions/` with `npx rayfin functions init`. ## Deploying with `rayfin up` [#deploying-with-rayfin-up] `rayfin up` is the canonical "deploy this app" command — it syncs the service flags and runtime settings from `rayfin.yml` to the remote Rayfin item, alongside applying your database schema and, when enabled, building and deploying static content: ```bash npx rayfin up ``` With `services.functions.enabled: true`, functions ship as part of this same deployment. Redeploy with `rayfin up` after changing function code. ## Deploying functions on their own [#deploying-functions-on-their-own] For faster iteration you can push functions without rebuilding and redeploying the rest of the app: ```bash npx rayfin up functions deploy ``` > [!NOTE] > This subcommand is marked experimental in the CLI. It is registered only when > `services.functions.enabled: true` in `rayfin.yml` (or `RAYFIN_FEATURE_FLAGS` includes > `functions`). | Flag | Description | | --------------- | -------------------------------------------------------- | | `-v, --verbose` | Enable verbose output. | | `--skip-build` | Skip the build command and deploy existing build output. | | `--json` | Output the result as JSON. | It is the functions equivalent of [`rayfin up staticapp deploy`](/docs/reference/cli/up) for static content and `rayfin up db apply` for schema. Use plain `rayfin up` when you want the whole app deployed consistently. ```prompt title="Deploy my Rayfin app including functions" Deploy my Rayfin app to Microsoft Fabric, functions included. First confirm services.functions.enabled is true in rayfin/rayfin.yml (and that rayfin/functions/ exists — run `npx rayfin functions init` if it doesn't). Then run the standard deploy workflow yourself: 1. `npx rayfin login` 2. `npx rayfin up` 3. `npx rayfin up status` After deploying, tell me how to call one of the functions from the frontend. ``` --- --- title: "Functions" description: "Run server-side TypeScript in Rayfin — when to use functions instead of client-side data access, and how to enable and scaffold them." url: https://rayfin.ai/docs/functions markdown_url: https://rayfin.ai/docs/functions.md section: functions product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: functions/index.mdx --- # Functions > Run server-side TypeScript in Rayfin — when to use functions instead of client-side data access, and how to enable and scaffold them. Rayfin functions are server-side TypeScript user-defined functions (UDFs) that run in the Fabric runtime and are invocable from your frontend through `RayfinClient` with full type safety, the same way `client.data.` is typed. > [!WARNING] > Functions are experimental and are not available in every Fabric region or tenant. > `@microsoft/rayfin-functions` may change substantially between releases. Confirm the > service deploys in your own workspace before you design an app around it. ## When to use functions [#when-to-use-functions] Reach for a function whenever logic needs to run on the backend rather than the frontend: * **Sensitive operations** — secrets, API keys, or privileged access to external resources that must never reach client-side code. * **Business logic that must not be tampered with** — anything a client could otherwise bypass or forge by calling your data API directly. * **Server-side validation** — checks that have to be trustworthy, not just present in the UI. * **Aggregation or transformation** — computing or reshaping data before it reaches the client, instead of shipping raw rows and doing the work in the browser. If a frontend feature needs trusted server-side behavior, implement it as a function rather than adding an ad hoc backend service. For everything else — reading and writing your own entities — use [`client.data.`](/docs/data/querying) directly; it's already authenticated and type-safe, and a function would only add a hop. ## How functions fit the architecture [#how-functions-fit-the-architecture] A function is registered with `udf.func(name, handler, [])` in your functions project. Inside the handler, `RayfinContext.getDataClient()` gives you the same typed data client used on the frontend (`.select().where().execute()`), so a function can read and write your entities with the same query chain you already know. Functions can also declare **connections** to external services — see [Connections](/docs/functions/connections) — so they can call out to Fabric-managed resources like SQL, Key Vault, or OneLake using delegated auth instead of long-lived secrets. From the frontend, a function is just another typed call: `client.functions..invoke()`. See [Calling functions from your app](/docs/functions/calling-functions) for the client side. ## Enable functions [#enable-functions] Set the `functions` flag in `rayfin/rayfin.yml`: ```yaml title="rayfin/rayfin.yml" services: functions: enabled: true ``` ## Scaffold a functions project [#scaffold-a-functions-project] ```bash npx rayfin functions init ``` This scaffolds `rayfin/functions/`, installs its dependencies, and generates the initial `types.ts` schema. Pass `--force` to re-scaffold, overwriting existing files: ```bash npx rayfin functions init --force ``` See [Writing a function](/docs/functions/writing-functions) for the project layout and how to register your first function. ## In this section [#in-this-section] * **[Writing a function](/docs/functions/writing-functions)** — project layout, `function_app.ts`, `RayfinContext`, and generated types. * **[Connections](/docs/functions/connections)** — delegated auth to external services like SQL, Key Vault, and OneLake. * **[Calling functions from your app](/docs/functions/calling-functions)** — the typed `FunctionClient` and error handling. * **[Deploying functions](/docs/functions/deploying)** — how functions ship alongside the rest of your app. ```prompt title="Add a Rayfin function for server-side logic" I need a piece of logic in my Rayfin app to run on the server instead of the client (explain what it does and why it shouldn't run in the browser). In rayfin/rayfin.yml, enable services.functions. Then run `npx rayfin functions init` if rayfin/functions/ doesn't exist yet, and add a new function in rayfin/functions/src/function_app.ts using udf.func(name, handler, []). Use RayfinContext if the function needs typed access to my entities via ctx.getDataClient(). After adding it, deploy it with `npx rayfin up` (or `npx rayfin up functions deploy` to push just the functions change) and tell me how to call it from the frontend. ``` --- --- title: "Writing a function" description: "The rayfin/functions project layout, registering functions with udf.func in function_app.ts, typed data access, and the auto-generated types.ts schema." url: https://rayfin.ai/docs/functions/writing-functions markdown_url: https://rayfin.ai/docs/functions/writing-functions.md section: functions product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: functions/writing-functions.mdx --- # Writing a function > The rayfin/functions project layout, registering functions with udf.func in function_app.ts, typed data access, and the auto-generated types.ts schema. This page covers the `rayfin/functions/` project itself — its layout, how to register a function, how to read and write your entities from inside one, and how its generated types work. > [!WARNING] > Functions are experimental and are not available in every Fabric region or tenant. See > [Functions](/docs/functions) before you depend on them. ## Project layout [#project-layout] After `npx rayfin functions init`, the functions project lives at `rayfin/functions/`: ```text rayfin/ data/ ← entity classes (shared via TS project references) functions/ src/ function_app.ts ← register functions here types.ts ← auto-generated by typegen — never hand-edit tsconfig.json ← references: [{ "path": ".." }] package.json host.json local.settings.json ``` `rayfin/functions/tsconfig.json` uses `composite: true` with a project reference to `rayfin/`, so functions can `import type` from your data entities without duplicating type definitions. ## Registering a function [#registering-a-function] Every function is registered with `udf.func(name, handler, [])` from `@microsoft/fabric-user-data-functions`: ```typescript title="rayfin/functions/src/function_app.ts" import { UserDataFunctions } from '@microsoft/fabric-user-data-functions'; const udf = new UserDataFunctions(); /** * A simple greeting function. * * The input/output types for this function are autogenerated in ./types.ts so that * RayfinClient can invoke it with full type-safety from your frontend app. */ udf.func('helloWorld', (firstName: string, lastName: string): string => { console.log(`helloWorld invoked for ${firstName} ${lastName}`); return `Hello ${firstName} ${lastName}!`; }, []); ``` * The first argument is the function name — it must match the key typegen produces in `AppFunctionsSchema`. * The second argument is the handler: typed parameters plus a return type, both extracted by typegen into `types.ts`. * The third argument is reserved for future middleware — pass an empty array, `[]`. * Always register functions with `udf.func(...)`. Don't export bare functions instead — typegen only sees registrations made through `udf.func`. ## Typed data access with `RayfinContext` [#typed-data-access-with-rayfincontext] Add a `RayfinContext` parameter to reach the same data client `client.data.` uses on the frontend — `.select().where().execute()`, the same chain throughout: ```typescript title="rayfin/functions/src/function_app.ts" import { UserDataFunctions, type RayfinContext } from '@microsoft/fabric-user-data-functions'; type AppSchema = { Entry: { id: string; message: string; createdAt: string }; }; const udf = new UserDataFunctions(); udf.func('getEntries', async ( ctx: RayfinContext ): Promise<{ id: string; message: string }[]> => { console.log('getEntries invoked'); const data = ctx.getDataClient(); return data.Entry.select(['id', 'message', 'createdAt']).execute(); }, []); udf.func('addEntry', async ( message: string, ctx: RayfinContext ): Promise => { console.log('addEntry invoked'); const data = ctx.getDataClient(); await data.Entry.create({ message }); }, []); ``` Pass your `AppSchema` type — the same schema type you already pass to `RayfinClient` on the frontend — as the generic parameter, so `getDataClient()` only allows valid entity names and fields. A bare `RayfinContext` (no generic) still works, but `getDataClient()` returns untyped (`Record`) access instead. `RayfinContext` is a runtime-injected parameter, so typegen automatically strips it from the generated input type — `addEntry`'s generated input is `{ message: string }`, not `{ message: string; ctx: RayfinContext }`. `RayfinContext` API: | Member | Description | | ---------------------------- | ---------------------------------------------------------------------------------------------------- | | `ctx.getDataClient()` | Returns the entity data client — typed when using `RayfinContext`. | | `ctx.baseUrl` | The Rayfin endpoint URL (readonly). | | `ctx.accessToken` | The auth token for the current request (readonly). | | `ctx.publishableKey` | The Rayfin publishable key (readonly). | | `ctx.getSecret(name)` | Returns a secret — see below. | | `ctx.getToken(audienceType)` | Returns a delegated token for an external resource — see [Connections](/docs/functions/connections). | * Import `RayfinContext` from `@microsoft/fabric-user-data-functions` — **not** from `@microsoft/rayfin-functions`. * Use `console.log(...)` / `console.error(...)` for logging, not `ctx.log`. * Import data entity classes with `import type`, not a runtime `import` — a runtime import of `@microsoft/rayfin-core` decorators would pull unnecessary dependencies into the functions bundle. Use the `.js` extension on relative imports, matching ESM resolution: ```typescript import type { TodoItem } from '../../data/TodoItem.js'; ``` This works because `rayfin/functions/tsconfig.json` has `"references": [{ "path": ".." }]` pointing at `rayfin/tsconfig.json`. ## Function secrets [#function-secrets] Use `ctx.getSecret(name)` when a function needs a secret value. The runtime checks host-provided invocation secrets first, then falls back to `process.env[name]`: ```typescript title="rayfin/functions/src/function_app.ts" udf.func('readApiKey', async (ctx: RayfinContext): Promise => { const apiKey = ctx.getSecret('THIRD_PARTY_API_KEY'); if (!apiKey) { throw new Error('Missing THIRD_PARTY_API_KEY secret.'); } return apiKey; }, []); ``` Set project secrets with `rayfin secret set `. ## Generated types [#generated-types] The CLI parses every `udf.func()` call under `rayfin/functions/src/` and generates `types.ts`: ```typescript title="rayfin/functions/src/types.ts" export type AppFunctionsSchema = { helloWorld: { input: { firstName: string; lastName: string }; output: string; }; getEntries: { input: void; // RayfinContext was stripped output: Entry[]; }; }; ``` * `rayfin functions init` runs typegen once after scaffolding, to seed `types.ts`. * Never hand-edit `types.ts` — it's regenerated from your `udf.func()` calls and any manual changes are overwritten. * `Promise` return types are unwrapped to `T` in the generated schema. See [Calling functions from your app](/docs/functions/calling-functions) for importing `AppFunctionsSchema` on the frontend. ```prompt title="Write a Rayfin function with typed data access" In my Rayfin project, add a new function to rayfin/functions/src/function_app.ts (running `npx rayfin functions init` first if that directory doesn't exist yet). Register it with udf.func('', handler, []) and give the handler a RayfinContext parameter so ctx.getDataClient() is fully typed against my entities — use the same AppSchema type my frontend already passes to RayfinClient. Import RayfinContext from @microsoft/fabric-user-data-functions, not @microsoft/rayfin-functions, and use import type for any data entity classes. After adding it, confirm it appears in the regenerated rayfin/functions/src/types.ts — never hand-edit that file — then deploy with `npx rayfin up` (or `npx rayfin up functions deploy`) and tell me how to call it from the frontend. ``` --- --- title: "Static content hosting" description: "Deploy your built frontend alongside your Rayfin backend with staticHosting in rayfin.yml — configuration, deployment, limits, and troubleshooting." url: https://rayfin.ai/docs/hosting markdown_url: https://rayfin.ai/docs/hosting.md section: hosting product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: hosting/index.mdx --- # Static content hosting > Deploy your built frontend alongside your Rayfin backend with staticHosting in rayfin.yml — configuration, deployment, limits, and troubleshooting. Rayfin can build, package, and serve your frontend as static content alongside your backend APIs. Once static hosting is enabled, `rayfin up` deploys your built assets to the Rayfin host, which serves them at a public URL — no separate static-hosting service to configure. ## How it works [#how-it-works] 1. Rayfin runs your configured build command (for example, `npm run build`). 2. The CLI validates that the output folder exists and contains files. 3. All files are packaged into a compressed ZIP archive (100 MB maximum). 4. The archive is uploaded to the Rayfin host, which extracts and serves the content. 5. The host returns a public hosting URL where your site is accessible. ## Configuration [#configuration] Add a `staticHosting` block under `services` in `rayfin.yml`: ```yaml title="rayfin/rayfin.yml" services: staticHosting: enabled: true folder: dist buildCommand: npm run build indexDocument: index.html ``` | Option | Required | Default | Description | | --------------- | -------- | ------------ | --------------------------------------------------------------------- | | `enabled` | Yes | — | Set to `true` to enable static hosting. | | `folder` | Yes | `"dist"` | Output folder containing built static files, relative to `root`. | | `root` | No | Project root | Root directory of the frontend project, relative to the project root. | | `buildCommand` | No | — | Shell command to run before packaging, e.g. `npm run build`. | | `indexDocument` | No | — | Default document to serve for directory requests, e.g. `index.html`. | ### A separate frontend directory [#a-separate-frontend-directory] If your frontend lives in a subdirectory, set `root`: ```yaml title="rayfin/rayfin.yml" services: staticHosting: enabled: true root: frontend folder: dist buildCommand: npm run build indexDocument: index.html ``` This resolves the output path to `/frontend/dist`. ## Deploying static content [#deploying-static-content] ### Full deployment with `rayfin up` [#full-deployment-with-rayfin-up] When you run `rayfin up`, static content deploys automatically as part of the full-stack deployment — the CLI builds your frontend, packages the output, and uploads it alongside your data and auth configuration. ```bash npx rayfin up ``` #### Skip static deployment during local dev [#skip-static-deployment-during-local-dev] When iterating locally with `npm run dev` (Vite serves the frontend directly), pass `--exclude-services staticHosting` to deploy the backend without rebuilding and uploading the static bundle: ```bash npx rayfin up --exclude-services staticHosting ``` This skips only the static build/package/deploy phase — runtime settings still get posted, so previously deployed static content keeps serving from Fabric. Scaffolded templates use this flag in their `npm run dev` script. ### Standalone static deployment [#standalone-static-deployment] Use `staticapp deploy` to redeploy only your static content, without rerunning the full `rayfin up` flow — useful when only frontend code changed and you want a faster iteration cycle: ```bash npx rayfin up staticapp deploy ``` Skip the build step if you've already built and just want to deploy the existing output: ```bash npx rayfin up staticapp deploy --skip-build ``` Add `-v` / `--verbose` for detailed logging: ```bash npx rayfin up staticapp deploy -v ``` > [!NOTE] > `staticapp deploy` requires an existing remote deployment. Run `rayfin up` at least once > first to provision the remote endpoint. ## Redirect URIs [#redirect-uris] When static hosting is enabled, `rayfin up` automatically registers the hosting URL's bare origin in `allowedRedirectUris` — this is required for the Fabric SSO `postMessage` handoff, even when interactive Fabric auth is disabled. See [Redirect URIs](/docs/hosting/redirect-uris) for the full explanation and what you still need to configure yourself. ## Deployment limits [#deployment-limits] * The compressed ZIP archive must not exceed **100 MB**. * The CLI uses maximum compression to minimize upload size. * If your build output exceeds the limit, exclude large assets or move binary files to [Storage](/docs/storage) instead of bundling them as static content. ## Complete example [#complete-example] A full `rayfin.yml` with static hosting, auth, and data all enabled: ```yaml title="rayfin/rayfin.yml" id: my-app name: my-app version: 1.0.0 services: auth: enabled: true allowedRedirectUris: - http://localhost:5173 data: enabled: true dialect: mssql staticHosting: enabled: true folder: dist buildCommand: npm run build indexDocument: index.html ``` ## Troubleshooting [#troubleshooting] ### Static folder not found [#static-folder-not-found] Verify that: * The `folder` path in `rayfin.yml` is correct and relative to `root` (or the project root if `root` isn't set). * Your build command ran successfully and produced output in the expected directory. ### Empty static folder [#empty-static-folder] An empty output folder usually means the build command didn't produce output. Run it manually to check for errors: ```bash npm run build ``` ### Deployment too large [#deployment-too-large] If the ZIP exceeds 100 MB: * Review your build output for unnecessary files — source maps, unoptimized images. * Configure your bundler to exclude development artifacts from the production build. * Move large binary assets to [Storage](/docs/storage) instead of bundling them as static content. ### No remote endpoint configured [#no-remote-endpoint-configured] `rayfin up staticapp deploy` requires an existing remote deployment. Run `rayfin up` first to provision it, then use `staticapp deploy` for subsequent updates. ```prompt title="Enable static hosting and deploy the frontend" In my Rayfin project, enable static hosting: - Add a staticHosting block to rayfin/rayfin.yml with enabled: true, folder: dist, a buildCommand matching my project's build script, and indexDocument: index.html. If my frontend lives in a subdirectory, set root accordingly. - Deploy with `npx rayfin up` and tell me the resulting hosting URL. - Explain that `npx rayfin up --exclude-services staticHosting` is what my dev script should use, since Vite already serves the frontend locally. If the deploy fails because the static folder is missing or empty, run my build command directly first and show me the error. ``` --- --- title: "Redirect URIs" description: "Configure allowedRedirectUris in rayfin.yml for auth callbacks and the Fabric SSO handoff, and understand what rayfin up appends automatically." url: https://rayfin.ai/docs/hosting/redirect-uris markdown_url: https://rayfin.ai/docs/hosting/redirect-uris.md section: hosting product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: hosting/redirect-uris.mdx --- # Redirect URIs > Configure allowedRedirectUris in rayfin.yml for auth callbacks and the Fabric SSO handoff, and understand what rayfin up appends automatically. `allowedRedirectUris` is the allow-list of origins and URLs Rayfin will redirect back to after an auth flow. It backs two different things — the Fabric SSO `postMessage` handoff and ordinary auth callbacks — so it matters for both a local Vite app and a deployed Fabric app. ## Configuring `allowedRedirectUris` [#configuring-allowedredirecturis] It lives under `services.auth` in `rayfin.yml`: ```yaml title="rayfin/rayfin.yml" services: auth: enabled: true allowedRedirectUris: - http://localhost:5173 ``` | Field | Type | Default | Description | | --------------------- | ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `allowedRedirectUris` | `string[]` | `["http://localhost:5173"]` | Allowed redirect URIs for auth callbacks and the Fabric SSO handoff. Must include the bare origin for Fabric auth. | ## What it's used for [#what-its-used-for] The Fabric SSO popup flow uses your app's bare origin (e.g. `http://localhost:5173`, with no path) as the `postMessage` target origin for the handoff code. See [Fabric SSO](/docs/auth/fabric-sso) for the full flow. Add every origin your app is actually served from — typically your local dev server and your deployed hosting URL — and nothing else. ## What `rayfin up` appends automatically [#what-rayfin-up-appends-automatically] When [static hosting](/docs/hosting) is enabled, `rayfin up` automatically registers the hosting URL's bare origin in `allowedRedirectUris` on every deploy — you don't add it yourself. This is required for the Fabric SSO `postMessage` handoff, even when interactive Fabric auth is disabled. For example, if your hosting URL is `https://bold-river-a3f1bc9d02-westus2.webapp.example.com`, the deploy tool adds it alongside whatever you already configured, so `rayfin.yml` ends up looking like this after the first deploy: ```yaml title="rayfin/rayfin.yml" services: auth: allowedRedirectUris: - http://localhost:5173 - https://bold-river-a3f1bc9d02-westus2.webapp.example.com ``` The deploy tool updates the configuration and pushes it to the backend during deployment — this is why a project's `rayfin.yml` typically grows a second, Fabric-hosted entry the first time you deploy, without anyone editing the file by hand. ## Keep the list tightly scoped [#keep-the-list-tightly-scoped] `allowedRedirectUris` is a security boundary, not a convenience list. Every origin in it can receive a Fabric SSO handoff or complete an auth callback on behalf of your app, so: * Only add origins your app is actually served from — your local dev server and your deployed hosting URL(s). * Don't add wildcard or third-party origins. * Review the list after copying a `rayfin.yml` between projects or environments — a stale entry from a previous deployment is a redirect target an attacker could try to exploit. ## Troubleshooting [#troubleshooting] * **Fabric SSO origin mismatch** — see the origin-related entries in [Fabric SSO troubleshooting](/docs/auth/fabric-sso#troubleshooting); most trace back to `returnOrigin` (or the deployed hosting origin) not matching what's registered here. * **Changes not taking effect** — `rayfin.yml` changes require a restart (`rayfin up`) before the new redirect URIs are honored. ```prompt title="Audit and tighten allowedRedirectUris" Look at services.auth.allowedRedirectUris in my Rayfin project's rayfin/rayfin.yml. List every origin currently in it and tell me, for each one, whether it looks like: my local dev server, a Fabric-hosted deployment origin that rayfin up would have added automatically, or something else that shouldn't be there. Flag anything that isn't clearly one of my own app's origins, and propose a trimmed list. Do not remove entries without showing me the before/after diff first. ``` --- --- title: "Recipes" description: "End-to-end walkthroughs that combine Rayfin's data, auth, and deployment features into complete application patterns." url: https://rayfin.ai/docs/recipes markdown_url: https://rayfin.ai/docs/recipes.md section: recipes product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: recipes/index.mdx --- # Recipes > End-to-end walkthroughs that combine Rayfin's data, auth, and deployment features into complete application patterns. Recipes are complete, working walkthroughs. Each one starts from a scaffolded project and ends with a working, deployable app, showing how Rayfin's data model, client, and auth pieces fit together in practice. * **[Build a todo app](/docs/recipes/todo-app)** — a per-user data model, a React UI, and a deploy to Microsoft Fabric. * **[Multi-tenant patterns](/docs/data/permissions#multi-tenant-patterns)** — scope entities per user and per organization with `@role` policies. * **[Testing a Rayfin app](/docs/recipes/testing)** — test data and auth logic without a live backend. --- --- title: "Testing a Rayfin app" description: "Test a Rayfin app's data and auth logic in Vitest without a live backend, using a swappable auth service and an in-memory fallback." url: https://rayfin.ai/docs/recipes/testing markdown_url: https://rayfin.ai/docs/recipes/testing.md section: recipes product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: recipes/testing.mdx --- # Testing a Rayfin app > Test a Rayfin app's data and auth logic in Vitest without a live backend, using a swappable auth service and an in-memory fallback. A Rayfin frontend can be fully tested — data operations and auth-gated UI included — without a deployed backend or network access. This walkthrough uses two seams already present in a scaffolded app: an injectable auth service, and a local-mode fallback in the data layer. ```prompt title="Test Rayfin data and auth logic without a live backend" In my Rayfin + React + Vite project, set up Vitest with jsdom (environment: 'jsdom') and a setup file that shims localStorage on globalThis so the Rayfin auth client doesn't throw in tests. My data service module (e.g. src/services/todos.ts) reads a getRayfinClient() plus an isLocalBackend() flag from src/services/rayfinClient.ts and falls back to an in-memory array when isLocalBackend() is true — in my tests, mock src/services/rayfinClient.ts with vi.mock so isLocalBackend always returns true, forcing that in-memory path so no network calls happen. My auth layer is injected through an IAuthService interface (signIn, signOut, getCurrentUser, initEmbeddedAuth, fabricAuthEnabled) passed into an AuthProvider component; in each test, construct a plain object implementing IAuthService with stubbed async methods and pass it as the authService prop instead of a real implementation. Write component tests with @testing-library/react that render through this stubbed AuthProvider and assert on rendered UI and on spies over the data service functions. ``` ## Test runner setup [#test-runner-setup] Vitest runs in `jsdom` so React components can render, with a path alias matching the app's own `@/*` imports and a setup file loaded before every test file: ```typescript title="vitest.config.ts" import react from '@vitejs/plugin-react-swc'; import { resolve } from 'path'; import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': resolve(import.meta.dirname, 'src'), }, }, test: { globals: true, environment: 'jsdom', include: ['src/**/*.{test,spec}.{ts,tsx}'], exclude: ['node_modules', 'dist'], setupFiles: ['./src/__tests__/setup.ts'], }, }); ``` ## Global test setup [#global-test-setup] `jsdom` doesn't provide a usable `localStorage`, and the Rayfin auth client reads and writes session data through it — the setup file shims one so the client doesn't throw, and clears it between tests so state doesn't leak across cases: ```typescript title="src/__tests__/setup.ts" import '@testing-library/jest-dom'; import { beforeEach } from 'vitest'; // Minimal localStorage shim so the Rayfin auth client can read/write tokens // inside jsdom without crashing. const localStorageMock = { store: {} as Record, getItem(key: string) { return this.store[key] ?? null; }, setItem(key: string, value: string) { this.store[key] = String(value); }, removeItem(key: string) { delete this.store[key]; }, clear() { this.store = {}; }, }; Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true, }); beforeEach(() => { localStorageMock.clear(); }); ``` ## The auth seam: IAuthService [#the-auth-seam-iauthservice] Production code never imports `MockAuthService` or `RayfinAuthService` directly — it depends on an `IAuthService` interface instead: ```typescript title="src/services/IAuthService.ts" /** Trimmed view of the authenticated user shown in the UI. */ export interface AuthUser { id: string; email: string; name: string; } /** * Auth service contract used by the React layer. * * Two implementations ship with this template: * * - {@link MockAuthService} — used when the API URL points at localhost. Signs into * a local backend with a fixture email and password — both out of scope for a * Fabric-only app; see "What MockAuthService actually does" below. * - {@link RayfinAuthService} — used once deployed. Wraps the Fabric * brokered auth flow from `@microsoft/rayfin-auth-provider-fabric`. * * `bootstrapAuth()` picks the right one from VITE_* env vars at startup. */ export interface IAuthService { /** * True when this service requires Fabric/Entra interactive sign-in. * The AuthPage uses this to choose its loading-state label. */ readonly fabricAuthEnabled: boolean; /** * Acquire a session interactively. For Fabric this opens the broker * popup and must be called from a user-gesture handler. */ signIn(): Promise; signOut(): Promise; /** Return the current session's user, or `null` if not signed in. */ getCurrentUser(): Promise; /** * Try to acquire a session via the embedded (iframe) Fabric flow without * any UI. Returns `null` when not running inside a Fabric iframe. */ initEmbeddedAuth(): Promise; } ``` At runtime, `bootstrapAuth()` picks between the two real implementations based on where the API URL points: ```typescript title="src/services/bootstrap.ts" import type { IAuthService } from './IAuthService'; import { MockAuthService } from './MockAuthService'; import { RayfinAuthService } from './RayfinAuthService'; import { initRayfinClient } from './rayfinClient'; function isLocalBackendUrl(url: string): boolean { try { const { hostname } = new URL(url); return hostname === 'localhost' || hostname === '127.0.0.1'; } catch { return false; } } /** * Read VITE_* env vars, initialize the Rayfin client, and return the right * auth service for the target backend. * * - Localhost API URL → {@link MockAuthService} * - Anything else → {@link RayfinAuthService} (requires VITE_FABRIC_* vars) */ export function bootstrapAuth(): IAuthService { const apiUrl = import.meta.env.VITE_RAYFIN_API_URL || 'http://localhost:5168'; const localDev = isLocalBackendUrl(apiUrl); const publishableKey = import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY; if (!publishableKey && !localDev) { throw new Error( 'VITE_RAYFIN_PUBLISHABLE_KEY environment variable is required' ); } const client = initRayfinClient({ baseUrl: apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`, publishableKey: publishableKey ?? 'local-dev-key', localDev, }); if (localDev) { return new MockAuthService(client); } const workspaceId = import.meta.env.VITE_FABRIC_WORKSPACE_ID; const projectId = import.meta.env.VITE_FABRIC_ITEM_ID; const fabricPortalUrl = import.meta.env.VITE_FABRIC_PORTAL_URL; if (!workspaceId || !projectId || !fabricPortalUrl) { throw new Error( 'Missing required Fabric config. Set VITE_FABRIC_WORKSPACE_ID, VITE_FABRIC_ITEM_ID, and VITE_FABRIC_PORTAL_URL.' ); } return new RayfinAuthService(client, { workspaceId, projectId, fabricPortalUrl, returnOrigin: window.location.origin, }); } ``` ## What `MockAuthService` actually does [#what-mockauthservice-actually-does] `bootstrapAuth()` picks `MockAuthService` whenever the configured API URL's hostname is `localhost` or `127.0.0.1` — including the default it falls back to (`http://localhost:5168`) when `VITE_RAYFIN_API_URL` is unset. In a fresh scaffold, that is the path the app takes until you point it at a deployed Fabric backend. `MockAuthService` is not an inert placeholder. Its `signIn()` authenticates with a fixture credential pair — a hardcoded email and a hardcoded password — against whatever backend the client was constructed with, and if that account doesn't exist yet on that backend, it registers the account first, then retries: ```typescript title="src/services/MockAuthService.ts (scaffolded default)" // Local-dev fixture credentials. The bundled local backend ships without // Fabric/Entra, so this auth service signs in with a shared dev account. // These values only ever reach a developer's local machine — never use // them in production. const MOCK_EMAIL = 'dev@contoso.com'; const MOCK_PASSWORD = 'LocalDev!Pass123'; ``` That is a credential-based sign-in against a local backend — both out of scope for a Fabric-only app (see [Fabric SSO is the only auth method](/docs/auth#fabric-sso-is-the-only-auth-method) — Fabric SSO is the only supported authentication method, and a Rayfin app has no local backend to sign in against). Leaving this file untouched means the app depends on that unsupported path any time `VITE_RAYFIN_API_URL` is unset or resolves to `localhost`/`127.0.0.1` — for example, in CI, or on a machine where the env file hasn't been set up yet — and a reader who never opens `MockAuthService.ts` has no reason to know that. For a Fabric-only app, do one of: * **Delete it.** Remove `MockAuthService.ts` and the `localDev` branch in `bootstrap.ts` so `bootstrapAuth()` always constructs `RayfinAuthService` (Fabric SSO). * **Replace its body with an in-memory double.** Keep the class and the seam, but make it genuinely local — no `RayfinClient`, no request to any backend, just a fixed in-memory user: ```typescript title="src/services/MockAuthService.ts (in-memory replacement — calls no backend)" import type { AuthUser, IAuthService } from './IAuthService'; const FAKE_USER: AuthUser = { id: 'local-dev-user', email: 'dev@example.com', name: 'Local Dev', }; /** * Zero-backend stand-in for local iteration. Never calls a backend — there is * no email/password exchange and nothing here points at a server. */ export class MockAuthService implements IAuthService { readonly fabricAuthEnabled = false; private signedIn = false; async signIn(): Promise { this.signedIn = true; return FAKE_USER; } async signOut(): Promise { this.signedIn = false; } async getCurrentUser(): Promise { return this.signedIn ? FAKE_USER : null; } async initEmbeddedAuth(): Promise { return null; } } ``` This keeps the exact seam the rest of this page tests against — `IAuthService` plus the constructor injection in `bootstrapAuth()` — without shipping a credentialed sign-in or an assumption that a local backend exists. It is the same shape as the ad hoc stub objects constructed inline in the tests below; the difference is this one lives in `src/services/` and is wired in by `bootstrapAuth()` instead of being constructed per test. Tests skip `bootstrapAuth()` entirely. Because the app takes its auth service as a prop (``), a test can construct a third, minimal implementation — a plain object with stubbed async methods — and inject that instead: ```tsx const stubAuthService: IAuthService = { fabricAuthEnabled: false, async signIn() { return { id: 'u1', email: 'dev@contoso.com', name: 'dev' }; }, async signOut() {}, async getCurrentUser() { return null; }, async initEmbeddedAuth() { return null; }, }; render( ); ``` No real sign-in flow, browser popup, or network call happens — `AuthProvider` only ever calls the methods on the interface, so a same-shaped stub is indistinguishable from a real implementation as far as the component tree is concerned. ## The data seam: isLocalBackend() [#the-data-seam-islocalbackend] `src/services/todos.ts` checks `isLocalBackend()` (from `rayfinClient.ts`) before every operation, and falls back to an in-memory array when it's `true` — the same fallback that lets the app run locally with no database configured. Tests force this path by mocking the `rayfinClient` module itself, so no `RayfinClient` instance is ever constructed and no request ever leaves the process: ```typescript title="src/__tests__/todos.test.ts" import { describe, expect, it, vi, beforeEach } from 'vitest'; vi.mock('@/services/rayfinClient', () => ({ isLocalBackend: () => true, getRayfinClient: vi.fn(), })); import { createTodo, deleteTodo, getTodos, updateTodo } from '@/services/todos'; describe('todos service (in-memory mode)', () => { beforeEach(async () => { // Drain any in-memory state left over from a previous test. for (const todo of await getTodos()) { await deleteTodo(todo.id); } }); it('creates, lists, updates, and deletes todos', async () => { expect(await getTodos()).toEqual([]); const created = await createTodo('write tests'); expect(created.title).toBe('write tests'); expect(created.isCompleted).toBe(false); const list = await getTodos(); expect(list).toHaveLength(1); expect(list[0]?.id).toBe(created.id); const updated = await updateTodo(created.id, { isCompleted: true }); expect(updated.isCompleted).toBe(true); await deleteTodo(created.id); expect(await getTodos()).toEqual([]); }); }); ``` `vi.mock('@/services/rayfinClient', ...)` must be declared before the `import` of the module under test — Vitest hoists `vi.mock` calls to the top of the file, but keeping the mock visually first avoids confusion about ordering. ## Testing a component that uses both seams [#testing-a-component-that-uses-both-seams] A component test combines the stub `IAuthService` with the same `rayfinClient` mock, then renders through `AuthProvider` and asserts on the UI plus spies over the service functions: ```tsx title="src/__tests__/HomePage.test.tsx" import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { AuthUser, IAuthService } from '@/services/IAuthService'; vi.mock('@/services/rayfinClient', () => ({ isLocalBackend: () => true, getRayfinClient: vi.fn(), })); import { AuthProvider } from '@/hooks/AuthContext'; import { HomePage } from '@/pages/HomePage'; import * as todosService from '@/services/todos'; const stubUser: AuthUser = { id: 'u1', email: 'dev@contoso.com', name: 'dev' }; const stubAuthService: IAuthService = { fabricAuthEnabled: false, async signIn() { return stubUser; }, async signOut() {}, async getCurrentUser() { return stubUser; }, async initEmbeddedAuth() { return stubUser; }, }; function renderHome() { return render( ); } describe('HomePage', () => { beforeEach(async () => { for (const todo of await todosService.getTodos()) { await todosService.deleteTodo(todo.id); } vi.restoreAllMocks(); }); it('shows the empty state once loaded', async () => { renderHome(); expect(await screen.findByText(/All caught up/i)).toBeInTheDocument(); }); it('adds a todo optimistically and renders it immediately', async () => { renderHome(); await screen.findByText(/All caught up/i); const createSpy = vi.spyOn(todosService, 'createTodo'); const input = screen.getByLabelText(/New todo title/i); fireEvent.change(input, { target: { value: 'buy milk' } }); fireEvent.submit(input.closest('form')!); expect(screen.getByText('buy milk')).toBeInTheDocument(); await waitFor(() => expect(createSpy).toHaveBeenCalledWith('buy milk')); }); it('rolls back and shows an error when a mutation fails', async () => { await todosService.createTodo('rollback me'); renderHome(); await screen.findByText('rollback me'); const failure = new Error('network down'); const deleteSpy = vi .spyOn(todosService, 'deleteTodo') .mockRejectedValueOnce(failure); fireEvent.click(screen.getByLabelText(/Delete todo/i)); expect(deleteSpy).toHaveBeenCalledTimes(1); await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('network down') ); expect(screen.getByText('rollback me')).toBeInTheDocument(); }); }); ``` Notice `stubAuthService.signIn` and `getCurrentUser` return a fixed `stubUser` here, rather than `null` — that signs the component tree in immediately on mount, so tests can assert on the authenticated `HomePage` UI without simulating a sign-in click first. Spying on `todosService.createTodo`/`deleteTodo` (rather than re-testing `todos.ts` itself, already covered above) confirms `HomePage` calls the service layer correctly and reacts to success and failure — the rest of the real test suite covers toggling, inline editing, and more failure-rollback cases with the same two seams. ## Running tests [#running-tests] ```bash npm run test ``` This runs `vitest run` — a single pass suitable for CI. Use `npx vitest` directly for watch mode during development. --- --- title: "Build a todo app" description: "Build a Fabric-authenticated todo app end to end, from a per-user data model through a React UI to a deploy on Microsoft Fabric." url: https://rayfin.ai/docs/recipes/todo-app markdown_url: https://rayfin.ai/docs/recipes/todo-app.md section: recipes product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: recipes/todo-app.mdx --- # Build a todo app > Build a Fabric-authenticated todo app end to end, from a per-user data model through a React UI to a deploy on Microsoft Fabric. This walkthrough builds a todo list where every signed-in user sees only their own items — a `Todo` entity secured with row-level security, a typed data client, and a React UI, ending with a live deploy to a Fabric app. ## What you'll build [#what-youll-build] * A `Todo` entity with a policy that scopes every row to the signed-in user. * A small data-access module wrapping `client.data.Todo` for list, create, update, and delete. * A React page and row component wired to that module with optimistic updates. * A deployed Fabric app serving the built frontend against the live backend. * A local dev loop for iterating on the frontend with `npm run dev` against that deployed backend. ```prompt title="Build a todo app with Rayfin" Scaffold a new Rayfin project called "todo-app" with `npm create @microsoft/rayfin@latest todo-app`, choosing the MSSQL dialect. In rayfin/data/Todo.ts, define a Todo entity with id (@uuid()), title (@text({ min: 1, max: 100 })), isCompleted (@boolean()), createdAt (@date()), and user_id (@text({ max: 128 })). Add @role('authenticated', '*', { policy: (claims, item) => claims.sub.eq(item.user_id) }) so each signed-in user can only read and write their own todos. Register Todo in rayfin/data/schema.ts. Add a src/services/todos.ts module with getTodos, createTodo, updateTodo, and deleteTodo functions that call client.data.Todo (select/orderBy/execute, create, update, delete), setting user_id from the signed-in user's session on create. Build a React page that lists todos, adds new ones, and toggles/edits/deletes existing ones, calling those service functions. Then run `npx rayfin login` and `npx rayfin up` to deploy, and `npx rayfin up status` to confirm it's live. Once deployed, run `npm run dev` to iterate on the frontend locally against that backend. ``` ## 1. Scaffold a project [#1-scaffold-a-project] ```bash npm create @microsoft/rayfin@latest todo-app ``` Choose the MSSQL dialect when prompted — Fabric apps support MSSQL only. Rayfin ships a bundled `todoapp` template with this exact entity, service module, and UI already wired up; pass it explicitly if you'd rather start from the finished result and read along: ```bash npm create @microsoft/rayfin@latest todo-app -- --template todoapp ``` The rest of this page builds the same thing from an empty project. ## 2. Define the Todo entity [#2-define-the-todo-entity] Add a `Todo` entity under `rayfin/data/`. Every field gets exactly one type decorator, and the class gets a permission decorator that controls who can read and write rows. ```typescript title="rayfin/data/Todo.ts" import { entity, role, text, boolean, date, uuid, } from '@microsoft/rayfin-core'; @entity() @role('authenticated', '*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Todo { @uuid() id!: string; @text({ min: 1, max: 100 }) title!: string; @boolean() isCompleted!: boolean; @date() createdAt!: Date; @text({ max: 128 }) user_id!: string; } ``` * `@role('authenticated', '*', { policy: ... })` grants every CRUD action to signed-in users, but the `policy` callback narrows every read and write to rows where `user_id` matches the caller's `sub` claim — this is the row-level security rule. * `user_id` is `@text()`, not `@uuid()`. It isn't a foreign key to another entity; it holds the caller's JWT `sub` claim, an opaque string identifier. Reserve `@uuid()` for fields that reference another entity's `id` through `@one()`/`@many()`. See [Multi-tenant patterns](/docs/data/permissions#multi-tenant-patterns) for more on this distinction and for organization-scoped variations of this pattern. * `@text({ min: 1, max: 100 })` caps the column width on MSSQL. Every `@text()` field needs an explicit `max` — omitting it produces an `NVARCHAR(MAX)` column that can break GraphQL schema generation. See [Deployment troubleshooting](/docs/deploy/troubleshooting#graphql-internal-server-error-after-a-successful-deploy). ## 3. Register it in the schema [#3-register-it-in-the-schema] `rayfin/data/schema.ts` maps entity names to their classes, so `RayfinClient` can provide a typed `client.data.Todo`: ```typescript title="rayfin/data/schema.ts" import { Todo } from './Todo.js'; export type TodoAppSchema = { Todo: Todo; }; export const schema = [Todo]; ``` Add every new entity to this map — both the `schema` array the CLI reads, and the `TodoAppSchema` type your frontend imports. ## 4. Configure rayfin.yml [#4-configure-rayfinyml] Enable the services this app needs: data (MSSQL), auth (Fabric SSO), and static hosting for the built frontend. ```yaml title="rayfin/rayfin.yml" id: todo-app name: todo-app version: 1.0.0 services: auth: enabled: true fabric: enabled: true allowedRedirectUris: - http://localhost:5173 data: enabled: true dialect: mssql storage: enabled: false staticHosting: enabled: true folder: dist buildCommand: npm run build indexDocument: index.html functions: enabled: false ``` See the [`rayfin.yml` reference](/docs/reference/config/rayfin-yml) for every field. You don't need to add a `publishable_key` — the CLI retrieves and writes that on your first deploy, and it can't be hand-edited. ## 5. Wire the client [#5-wire-the-client] Wrap `RayfinClient` in a small module that initializes it once and exposes it to the rest of the app: ```typescript title="src/services/rayfinClient.ts" import { RayfinClient } from '@microsoft/rayfin-client'; import type { TodoAppSchema } from '../../rayfin/data/schema'; export interface RayfinClientConfig { baseUrl: string; publishableKey: string; /** True when the API URL points at localhost. Exposed via {@link isLocalBackend}. */ localDev: boolean; } let client: RayfinClient | null = null; let localDev = false; export function initRayfinClient( config: RayfinClientConfig ): RayfinClient { if (client) { throw new Error('Rayfin client is already initialized.'); } client = new RayfinClient({ baseUrl: config.baseUrl, publishableKey: config.publishableKey, useProxy: false, authStorage: true, }); localDev = config.localDev; return client; } export function getRayfinClient(): RayfinClient { if (!client) { throw new Error( 'Rayfin client not initialized. Call bootstrapAuth() first.' ); } return client; } /** True when the app was bootstrapped against a localhost API URL. */ export function isLocalBackend(): boolean { return localDev; } ``` `initRayfinClient` is called once at startup — typically from a `bootstrapAuth()`-style function that reads `VITE_RAYFIN_API_URL` and `VITE_RAYFIN_PUBLISHABLE_KEY` (generated by `rayfin env`) and also picks the right auth implementation for the target backend. Auth wiring is its own topic — see [Auth](/docs/auth) — this recipe focuses on the data path. ## 6. Build the data access layer [#6-build-the-data-access-layer] Wrap every `client.data.Todo` call in a small service module. This is also where the app decides *whose* todos it's reading or writing — `user_id` is set from the signed-in session on create, never accepted from the caller. ```typescript title="src/services/todos.ts" import { getRayfinClient, isLocalBackend } from './rayfinClient'; export interface TodoItem { id: string; title: string; isCompleted: boolean; createdAt: Date; } // Local-dev fallback: when no Fabric backend is configured, keep todos in // memory so the sample is fully functional without a database. let inMemoryTodos: TodoItem[] = []; export async function getTodos(): Promise { if (isLocalBackend()) { return [...inMemoryTodos].sort( (a, b) => b.createdAt.getTime() - a.createdAt.getTime() ); } const client = getRayfinClient(); const results = await client.data.Todo.select([ 'id', 'title', 'isCompleted', 'createdAt', ]) .orderBy({ createdAt: 'desc' }) .execute(); return results as TodoItem[]; } export async function createTodo(title: string): Promise { if (isLocalBackend()) { const todo: TodoItem = { id: crypto.randomUUID(), title, isCompleted: false, createdAt: new Date(), }; inMemoryTodos.push(todo); return todo; } const client = getRayfinClient(); const session = client.auth.getSession(); if (!session.isAuthenticated || !session.user) { throw new Error('Cannot create todo: user is not authenticated.'); } const todo = await client.data.Todo.create({ title, isCompleted: false, createdAt: new Date(), user_id: session.user.id, }); return todo as TodoItem; } export async function updateTodo( id: string, updates: Partial> ): Promise { if (isLocalBackend()) { const todo = inMemoryTodos.find((t) => t.id === id); if (!todo) throw new Error('Todo not found'); Object.assign(todo, updates); return { ...todo }; } const client = getRayfinClient(); await client.data.Todo.update({ id }, updates); const todo = await client.data.Todo.findById(id); return todo as TodoItem; } export async function deleteTodo(id: string): Promise { if (isLocalBackend()) { inMemoryTodos = inMemoryTodos.filter((t) => t.id !== id); return; } const client = getRayfinClient(); await client.data.Todo.delete({ id }); } ``` * `getTodos` uses the query chain `.select([...]).orderBy(...).execute()` — select only the fields the UI needs, and always specify an order. * `createTodo` reads the caller's ID from `client.auth.getSession()` and sets it as `user_id` — the server-side policy from step 2 then enforces that this user can only ever act on rows with a matching `user_id`. * `updateTodo` and `deleteTodo` filter by `{ id }`; the `Todo` entity's policy still applies underneath, so a request for someone else's row matches nothing. * The `isLocalBackend()` branches keep an in-memory array instead of calling the real client. The production app never hits this path once deployed — it exists so the app (and its tests) can run without a live backend. See [Testing a Rayfin app](/docs/recipes/testing) for how this fallback is used in tests. ## 7. Build the React UI [#7-build-the-react-ui] A row component renders a single todo, with inline editing, a completion toggle, and delete: ```tsx title="src/components/TodoRow.tsx" import { useEffect, useRef, useState } from 'react'; import type { TodoItem } from '@/services/todos'; interface TodoRowProps { todo: TodoItem; onToggle: (id: string, isCompleted: boolean) => void; onDelete: (id: string) => void; onEdit: (id: string, title: string) => void; } export function TodoRow({ todo, onToggle, onDelete, onEdit }: TodoRowProps) { const [isEditing, setIsEditing] = useState(false); const [draft, setDraft] = useState(todo.title); const inputRef = useRef(null); useEffect(() => { if (!isEditing) setDraft(todo.title); }, [todo.title, isEditing]); useEffect(() => { if (isEditing) { inputRef.current?.focus(); inputRef.current?.select(); } }, [isEditing]); const startEdit = () => { setDraft(todo.title); setIsEditing(true); }; const cancelEdit = () => { setDraft(todo.title); setIsEditing(false); }; const commitEdit = () => { const next = draft.trim(); if (!next || next === todo.title) { cancelEdit(); return; } onEdit(todo.id, next); setIsEditing(false); }; return (
  • {isEditing ? ( setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commitEdit(); } else if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); } }} onBlur={commitEdit} aria-label="Edit todo title" className="flex-1 rounded-md border border-blue-300 bg-white px-2 py-1 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" /> ) : ( )} {!isEditing && (
    )}
  • ); } ``` The page ties `todos.ts` and `TodoRow` together. It loads the list on mount, and every mutation updates local state optimistically before the request resolves — rolling back and showing an error if the request fails: ```tsx title="src/pages/HomePage.tsx" import { useCallback, useEffect, useRef, useState } from 'react'; import { TodoRow } from '@/components/TodoRow'; import { useAuth } from '@/hooks/AuthContext'; import { createTodo, deleteTodo, getTodos, updateTodo, type TodoItem, } from '@/services/todos'; export function HomePage() { const { signOut, user } = useAuth(); const [todos, setTodos] = useState([]); const [newTodoTitle, setNewTodoTitle] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const errorTimeoutRef = useRef | null>(null); const showError = useCallback((message: string) => { setError(message); if (errorTimeoutRef.current) clearTimeout(errorTimeoutRef.current); errorTimeoutRef.current = setTimeout(() => setError(null), 5000); }, []); useEffect(() => { return () => { if (errorTimeoutRef.current) clearTimeout(errorTimeoutRef.current); }; }, []); const fetchTodos = useCallback(async () => { try { const data = await getTodos(); setTodos(data); } catch (err) { const message = err instanceof Error ? err.message : 'Failed to load todos.'; showError(message); } finally { setLoading(false); } }, [showError]); useEffect(() => { void fetchTodos(); }, [fetchTodos]); const handleAddTodo = async (e: React.FormEvent) => { e.preventDefault(); const title = newTodoTitle.trim(); if (!title) return; const tempId = `temp-${crypto.randomUUID()}`; const optimistic: TodoItem = { id: tempId, title, isCompleted: false, createdAt: new Date(), }; setNewTodoTitle(''); setTodos((prev) => [optimistic, ...prev]); try { const created = await createTodo(title); setTodos((prev) => prev.map((t) => (t.id === tempId ? { ...created } : t)) ); } catch (err) { setTodos((prev) => prev.filter((t) => t.id !== tempId)); setNewTodoTitle(title); const message = err instanceof Error ? err.message : 'Failed to add todo.'; showError(message); } }; const handleToggle = (id: string, isCompleted: boolean) => { const snapshot = todos; setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, isCompleted: !isCompleted } : t)) ); void updateTodo(id, { isCompleted: !isCompleted }).catch((err) => { setTodos(snapshot); const message = err instanceof Error ? err.message : 'Failed to update todo.'; showError(message); }); }; const handleDelete = (id: string) => { const snapshot = todos; setTodos((prev) => prev.filter((t) => t.id !== id)); void deleteTodo(id).catch((err) => { setTodos(snapshot); const message = err instanceof Error ? err.message : 'Failed to delete todo.'; showError(message); }); }; const handleEdit = (id: string, title: string) => { const snapshot = todos; setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, title } : t))); void updateTodo(id, { title }).catch((err) => { setTodos(snapshot); const message = err instanceof Error ? err.message : 'Failed to save todo.'; showError(message); }); }; const pending = todos.filter((t) => !t.isCompleted); const completed = todos.filter((t) => t.isCompleted); const remainingLabel = pending.length === 0 ? 'All clear — nothing pending' : `${pending.length} ${pending.length === 1 ? 'task' : 'tasks'} pending`; return (

    Todo App

    {user?.email && ( {user.email} )}
    void handleAddTodo(e)} className="flex gap-3 mb-3" > setNewTodoTitle(e.target.value)} placeholder="What needs to be done?" aria-label="New todo title" maxLength={100} className="flex-1 rounded-xl border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-400 shadow-sm transition-all focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
    {!loading && todos.length > 0 && (

    {remainingLabel}

    )} {error && (
    {error}
    )} {loading ? (

    Loading...

    ) : todos.length === 0 ? (

    All caught up!

    Add your first todo above to get started.

    ) : (
    {pending.length > 0 && (

    To Do ({pending.length})

      {pending.map((todo) => ( ))}
    )} {completed.length > 0 && (

    Completed ({completed.length})

      {completed.map((todo) => ( ))}
    )}
    )}
    ); } ``` > [!NOTE] > This is trimmed for length — the shipped template's `HomePage` additionally renders a > bulk "add 100 samples" and "clear all" action pair for stress-testing the list (built on > the same `todos.ts` functions) between the add form and the todo list below, and a > decorative icon above the empty state. The add/toggle/edit/delete loop above, and every > other class name and attribute shown, are otherwise unchanged from the shipped file. ## 8. Apply the schema [#8-apply-the-schema] Once the entity and its registration exist, push the schema to your backend: ```bash npx rayfin up db apply ``` The first time you run this against a project with no prior deployment, it needs a deployed target to apply to — the next step's `rayfin up` handles both the first deploy and the first schema apply together. Come back to `db apply` on its own for every schema change after that. ## 9. Deploy [#9-deploy] ```bash npx rayfin login npx rayfin up npx rayfin up status ``` `rayfin up` creates the Fabric app on the first run, applies the `Todo` schema, builds and uploads the React frontend, and prints the live hosting URL. See [Deploying with rayfin up](/docs/deploy/rayfin-up) for the full workflow, flags, and what gets written to `rayfin/.deployments.json`. ## 10. Iterate on the frontend locally [#10-iterate-on-the-frontend-locally] With the backend deployed, serve the frontend locally instead of rebuilding and uploading it on every change: ```bash npm run dev ``` The scaffolded `predev` script regenerates `.env.local` from the values `rayfin up` wrote to `rayfin/.env`, so Vite serves the frontend at `http://localhost:5173` against the backend you just deployed. For a subsequent backend change — a new field on `Todo`, for example — redeploy with `npx rayfin up --exclude-services staticHosting` so the schema and runtime settings update without rebuilding the static bundle Vite is already serving locally. See [Static content hosting](/docs/hosting#skip-static-deployment-during-local-dev) for the full explanation. ## Next steps [#next-steps] * [Multi-tenant patterns](/docs/data/permissions#multi-tenant-patterns) — extend this pattern to data shared across a team or organization. * [Testing a Rayfin app](/docs/recipes/testing) — test this exact data and auth setup without a live backend. * [Deployment troubleshooting](/docs/deploy/troubleshooting) — fixes for the most common deploy failures. --- --- title: "Rules for coding agents" description: "The condensed set of rules and anti-patterns for writing Rayfin code — read this before generating entities, queries, permissions, or deployment commands." url: https://rayfin.ai/docs/reference/agent-rules markdown_url: https://rayfin.ai/docs/reference/agent-rules.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/agent-rules.mdx --- # Rules for coding agents > The condensed set of rules and anti-patterns for writing Rayfin code — read this before generating entities, queries, permissions, or deployment commands. This is the short version of everything on this site that changes whether generated Rayfin code works. If you are an agent and you read only one page, read this one. Full detail lives in [Known limitations](/docs/reference/known-limitations), [Modeling entities](/docs/data/modeling), and [Permissions](/docs/data/permissions). ## Platform [#platform] * Rayfin uses **TC39 Stage 3 decorators**. Never enable `experimentalDecorators` or `emitDecoratorMetadata`. * Include `ESNext.Decorators` in the tsconfig `lib` array. * Rayfin has one deployment target: a managed **Fabric app** (MSSQL only). * Prefer `npm create @microsoft/rayfin@latest` for new projects — it generates a correct tsconfig and schema boilerplate. ## Data modeling [#data-modeling] * Define entities with `@entity()` in `rayfin/data/`, and register each one in `rayfin/data/schema.ts` as `type AppSchema = { Todo: Todo }`. * Every field needs exactly one type decorator: `@uuid`, `@text`, `@int`, `@decimal`, `@boolean`, `@date`, `@email`, `@set`. * Fields are **required by default**. For a nullable field use `{ optional: true }` and `?` together. * `@text()`'s length option is `max`, not `maxLength` — `@text({ max: 200 })`. Always set it on MSSQL; see [Known limitations](/docs/reference/known-limitations#text-without-max-breaks-graphql-schema-generation-on-mssql). * Use `@one(() => Target)` with a lazy arrow function for relationships. Rayfin auto-generates the foreign key column, named `{property}_id` — see [Known limitations](/docs/reference/known-limitations#foreign-keys-must-use-the-property_id-naming-convention). * Use `import`, not `import type`, for entity classes referenced inside `@one()` / `@many()` arrow functions — the decorator needs the runtime class value. * A foreign key column referencing another entity (`{property}_id`) must be `@uuid()` to match the primary key type. An auth-derived field such as `user_id` from `claims.sub` is `@text()`, not a foreign key. * Many-to-many is not supported. Use an explicit join entity with two `@one()` fields. See [Known limitations](/docs/reference/known-limitations#many-to-many-relationships-are-not-supported). ## Security [#security] * **Every entity needs an explicit permission decorator** (`@role`, `@anonymous`, `@authenticated`). Omitting one silently applies `authenticated: *` — full CRUD for any signed-in user — which is almost never what you want in production. * Add a policy for row-level filtering on user-scoped data: `policy: (claims, item) => claims.sub.eq(item.user_id)`. * Use `exclude` in role options to hide sensitive fields, e.g. `exclude: ['secret']`. * Publishable keys (`pk-*`) are safe in client-side code. Never expose service secrets or connection strings. * Keep `allowedRedirectUris` in `rayfin.yml` scoped to your app's own origins. * Fabric SSO (Entra ID) is the only supported authentication method. Its popup flow works from any registered origin, including a local Vite dev server (`http://localhost:5173`); only the embedded (iframe) flow requires running inside the Fabric portal itself. ## Querying [#querying] * The query chain is `.select()` → `.where()` → `.orderBy()` → `.execute()`. * Fetch a single record with `client.data.Entity.findById('uuid')` — there is no `findByPk`. * Filter by foreign key columns using `{property}_id` (`customer_id`), not a dot-path (`customer.id`). Dot-paths are for `.select()` only. * Sort directions are lowercase: `'asc'` or `'desc'`. * Use `.first(n).executePaginated()` with `.after(endCursor)` for pagination. Collection queries return a single page otherwise; the default page size is 100. * `count()` does not exist on the fluent client. `count` is an [aggregation](/docs/data/aggregations) operation and, like `sum`/`avg`/`min`/`max`, accepts **numeric fields only** — so it counts non-null numeric values, not rows. For a row count, select minimal fields and use `results.length`. See [Known limitations](/docs/reference/known-limitations#count-counts-numeric-values-not-rows). * Grouped aggregation cannot be combined with `.select()`, `.orderBy()`, `.first()` or `.after()` — Data API Builder rejects `groupBy` alongside `items`. Sort the returned array in TypeScript instead. ## Auth [#auth] * The session change callback is `onSessionChange`. **`onAuthStateChange` does not exist.** See [Known limitations](/docs/reference/known-limitations#the-callback-is-onsessionchange-not-onauthstatechange). * Session objects are opaque. Gate UI on `isAuthenticated` or the presence of a `user` property — do not introspect the session. * After enabling or disabling auth in `rayfin.yml`, restart the backend so the updated endpoints are exposed. ## Configuration [#configuration] * Always declare `services.auth` and `services.data` explicitly in `rayfin.yml`, even as `enabled: false`. The CLI reads those keys without guarding and does not apply defaults. * If `services.data.enabled` is `true`, `dialect` is required — `mssql` is the only supported value. See [Known limitations](/docs/reference/known-limitations#dialect-is-required-when-the-data-module-is-enabled). ## Connectors [#connectors] Connectors reach data that already exists in Fabric. They are in private preview and behind a feature flag — read [Connectors](/docs/connectors) before writing any connector code. * The `rayfin connector` command group is not registered until the project sets `services.connectors.enabled: true` in `rayfin.yml`, has a non-empty `connectors:` block, or runs with `RAYFIN_FEATURE_FLAGS=connectors`. * The `connector add` flag is `--type`, not `--connector`. The five type literals are `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`, `fabric-semanticmodel`, and `kusto`. **There is no `fabric-sql`.** * `connector add` scaffolds files but installs nothing. Run the version-pinned `npm install` it prints, verbatim — never drop the version. * `connector add` does **not** emit entity `.ts` files. Generate them yourself from `rayfin/connectors//metadata.json`, following [Generating entity files](/docs/connectors/entity-generation). `metadata.json` is the only source of truth for primary keys and relationships — absent metadata means keyless, not permission to infer a key from column names. * In `rayfin/connectors//schema.ts`, import and re-export entity classes with `import type` / `export type` only. A value import ships decorated classes into the browser bundle; the build and deploy both succeed and the deployed page renders blank. * `GraphQLBackedConnector` is the marker for all three Category A types. Per-type names like `FabricWarehouse` do not exist. * Import `ConnectorsRayfinClient` from `@microsoft/rayfin-client/experimental`, never the stable `@microsoft/rayfin-client` entry. * Category B connectors need the runtime map as the client's second constructor argument — `kusto()` injects cluster routing and `fabricSemanticModel()` decodes the response. Omit it and calls do not work. * `auth.type` is lowercase, `delegated` or `application`. `application` is rejected on `fabric-semanticmodel` and `kusto`. See [Connector authentication](/docs/connectors/auth). ## Deployment [#deployment] * `rayfin up` is the canonical command for "deploy this change", "apply this schema change", or "push my entity update". It builds and deploys the static app **and** applies pending schema migrations in one step. * `rayfin up db apply` is a narrow advanced subcommand that applies schema only. Use it when you explicitly want to skip the static deploy. * After adding or changing an entity, verify with `rayfin up status`. A deploy can report success while a newly added entity is not yet readable. * `--force` permits destructive changes (drop table, drop column, alter type). Never use it without review. * When a user asks you to "build and deploy" or "make it live", run the workflow — `rayfin login` → `rayfin up` → `rayfin up status` — rather than printing steps for them to run. ## Anti-patterns [#anti-patterns] | Don't | Do | | -------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Raw `fetch()` or hand-built GraphQL for data | `client.data.` — typed queries with automatic auth | | Entities with no permission decorator | An explicit `@anonymous()`, `@authenticated()`, or `@role()` | | `@text()` with no `max` on MSSQL | `@text({ max: N })` — `NVARCHAR(MAX)` breaks GraphQL schema generation | | `@text()` for a foreign key column | `@uuid()`, to match the referenced primary key | | `onAuthStateChange` | `onSessionChange` | | `findByPk` | `findById` | | Guessing at an API surface | Fetch the relevant `.md` page, or use the [MCP server](/docs/reference/cli/docs#mcp-server) | --- --- title: "Deprecation warnings" description: "How to silence Rayfin's deprecation warnings in application code, in Node.js scripts, and in browser apps." url: https://rayfin.ai/docs/reference/deprecations markdown_url: https://rayfin.ai/docs/reference/deprecations.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T00:07:16-07:00 source: reference/deprecations.mdx --- # Deprecation warnings > How to silence Rayfin's deprecation warnings in application code, in Node.js scripts, and in browser apps. Rayfin can emit deprecation warnings when your application uses an API or option that will change in a future release. Warnings are printed with `console.warn` and include a stable code such as `[RAYFIN_DEP_USE_PROXY]`, so you can search your codebase for the affected usage. > [!NOTE] > The APIs on this page — `setDeprecationsSilenced`, `isDeprecationSilenced`, and > `RAYFIN_NO_DEPRECATION` — ship in `@microsoft/rayfin-client` **1.34 and later**. If your > installed version predates that, these exports won't exist yet; confirm with > `rayfin docs get --symbol setDeprecationsSilenced` or the > [MCP server](/docs/reference/cli/docs#mcp-server) before relying on them. ## Silence warnings in application code [#silence-warnings-in-application-code] Call `setDeprecationsSilenced(true)` once you've reviewed the warnings and want to hide them for a specific environment. Import it from `@microsoft/rayfin-client` and call it early in application startup, before creating Rayfin clients or using deprecated APIs. ```typescript import { setDeprecationsSilenced } from '@microsoft/rayfin-client'; setDeprecationsSilenced(true); ``` Call `setDeprecationsSilenced(false)` to turn warnings back on, and `isDeprecationSilenced()` to check the current setting: ```typescript import { isDeprecationSilenced } from '@microsoft/rayfin-client'; console.log(isDeprecationSilenced()); ``` ## Silence warnings in Node.js [#silence-warnings-in-nodejs] For Node.js scripts, tests, or server-side tooling, set `RAYFIN_NO_DEPRECATION` to `1` or `true` in the environment: ```bash RAYFIN_NO_DEPRECATION=1 npm run dev ``` ## Silence warnings in browser apps [#silence-warnings-in-browser-apps] Browser environments have no Node.js process environment, so `RAYFIN_NO_DEPRECATION` has no effect there. Use the programmatic toggle instead, and call it during application startup — before any code path that could emit a deprecation warning runs. ```typescript title="src/main.ts" import { setDeprecationsSilenced } from '@microsoft/rayfin-client'; if (import.meta.env.PROD) { setDeprecationsSilenced(true); } ``` ## See also [#see-also] * [Known limitations](/docs/reference/known-limitations) for current API constraints. * [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client) for the rest of the client configuration surface. --- --- title: "Errors and troubleshooting" description: "A symptom-to-fix index of the errors Rayfin builders hit most — deployment, schema apply, secrets, static hosting, and Fabric auth." url: https://rayfin.ai/docs/reference/errors markdown_url: https://rayfin.ai/docs/reference/errors.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/errors.mdx --- # Errors and troubleshooting > A symptom-to-fix index of the errors Rayfin builders hit most — deployment, schema apply, secrets, static hosting, and Fabric auth. This page consolidates the troubleshooting sections scattered across the Rayfin guide into one symptom → cause → fix index. Search it by the error text you're seeing, then jump to the matching heading for the full fix. ## Quick reference [#quick-reference] | Symptom | Cause | Fix | | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | "Internal server error" from the data API, after a deploy that reported success | `@text()` field with no `max` produced `NVARCHAR(MAX)` on MSSQL | Add `@text({ max: N })` to every string field, then reapply the schema | | `Dialect is required when Data module is enabled` (400 at deploy time) | `services.data.enabled: true` with no `dialect` in `rayfin.yml` | Set `dialect: mssql` | | `Failed to acquire authentication token` | Not signed in, or no OS credential storage available | `rayfin login`; on restricted systems set `RAYFIN_ENCRYPTION_FALLBACK_ENABLED=true` | | `rayfin up db apply` refuses to run, warns of data loss | The pending change drops a column/table or alters a type | Review the change; re-run with `--force` only once you accept the loss | | A newly added entity isn't readable right after a deploy that reported success | The schema migration step hadn't finished applying, or the client/cache is stale | Run `rayfin up status`; confirm schema apply completed before querying the new entity | | Deployment fails with `401` or `403` | Your Fabric session expired | `rayfin login`, then retry `rayfin up` | | `No remote endpoint configured` from `staticapp deploy` | No full deployment has happened yet | Run `rayfin up` once, then use `staticapp deploy` for subsequent updates | | Static deploy exceeds the size limit | Build output includes source maps or dev-only assets | Exclude dev artifacts from the production build, or move binaries to storage | | `rayfin secret set` exits without prompting | stdin is not a TTY, or `CI=true` — the command is interactive only | Run it from an interactive terminal; there is no CI path today | | Popup blocked during Fabric sign-in | `ensureSignedInWithFabric` was called outside a user-gesture handler | Call it directly from a button's `onClick` | ## Deployment and schema apply [#deployment-and-schema-apply] ### "Internal server error" after a successful deploy [#internal-server-error-after-a-successful-deploy] **Symptom:** `rayfin up` (or `rayfin up db apply`) reports success, but the GraphQL or REST data API returns a generic "Internal server error" at runtime. **Cause:** a `@text()` field with no `max` option produces an `NVARCHAR(MAX)` column on MSSQL. Rayfin's metadata provider can fail to build a GraphQL schema from that column type. **Fix:** add an explicit length to every string field targeting MSSQL — `@text({ max: 200 })` rather than bare `@text()` — then reapply the schema (`npx rayfin up db apply --force` if the column already exists with the wrong type). See [`@microsoft/rayfin-core`](/docs/reference/sdk/rayfin-core) and [Known limitations](/docs/reference/known-limitations). ### "Dialect is required when Data module is enabled" [#dialect-is-required-when-data-module-is-enabled] **Symptom:** `rayfin up` fails with a 400 error during deployment. **Cause:** `rayfin.yml` has `services.data.enabled: true` but no `dialect` key. **Fix:** add `dialect: mssql` (Fabric apps support MSSQL only) under `services.data`: ```yaml title="rayfin/rayfin.yml" services: data: enabled: true dialect: mssql ``` ### Database apply refuses to run (potential data loss) [#database-apply-refuses-to-run-potential-data-loss] **Symptom:** `rayfin up db apply` stops and warns instead of applying the change. **Cause:** the pending schema change would drop a column, drop a table, or alter a column's type — all are treated as potentially destructive. **Fix:** review the listed operations. If the data loss is acceptable, re-run with `--force`: ```bash npx rayfin up db apply --force ``` Never pass `--force` without reviewing what will be dropped. ### A newly added entity isn't readable after a deploy that reported success [#a-newly-added-entity-isnt-readable-after-a-deploy-that-reported-success] **Symptom:** `rayfin up` prints success, but querying a newly added entity immediately afterward returns nothing, or errors as if the entity doesn't exist. **Cause:** the static app and runtime settings can finish deploying slightly before the database schema migration for the new entity has fully applied, or a client built before the deploy is still caching the old schema. **Fix:** run `npx rayfin up status` and confirm the deployment is fully healthy before relying on the new entity. Refresh or rebuild the frontend client so it isn't holding a stale schema. ### Deployment fails with 401 or 403 [#deployment-fails-with-401-or-403] **Symptom:** `rayfin up` fails partway through with an authorization error. **Cause:** your signed-in Fabric session has expired. **Fix:** `npx rayfin login` to reauthenticate, then retry `npx rayfin up`. ## Sign-in and secrets [#sign-in-and-secrets] ### Sign-in or keychain failures [#sign-in-or-keychain-failures] **Symptom:** `rayfin login` or any command needing authentication fails with `Failed to acquire authentication token` or a similar credential-storage error. **Cause:** you aren't signed in, or the current environment (a container, a restricted CI runner) has no OS-backed credential storage for the CLI's token cache. **Fix:** run `npx rayfin login` — the account picker is always shown, so you can choose a different account. On systems without OS credential storage, set `RAYFIN_ENCRYPTION_FALLBACK_ENABLED=true` (development environments only — this stores the token cache in plaintext). ### `rayfin secret set` refuses to prompt [#rayfin-secret-set-refuses-to-prompt] **Symptom:** `rayfin secret set ` exits without asking for a value. **Cause:** stdin is not a TTY, or `CI=true` is set. The command reads the value from a masked prompt and has no non-interactive mode. **Fix:** run it from an interactive terminal. There is no supported way to set secrets from CI today. See [Secrets](/docs/deploy/secrets). ### Permission denied setting or listing secrets [#permission-denied-setting-or-listing-secrets] **Symptom:** `rayfin secret set` or `rayfin secret list` fails with a permission error. **Cause:** the signed-in account doesn't have access to the target Fabric workspace, or the project has not been deployed yet. **Fix:** run `npx rayfin up` first so the workload exists. Then confirm you're signed in with an account that has workspace access — run `npx rayfin login` again, the account picker is always shown. ## Static hosting [#static-hosting] ### Static folder not found / empty static folder [#static-folder-not-found--empty-static-folder] **Symptom:** deployment fails saying the static output folder doesn't exist, or it exists but is empty. **Cause:** the `folder` path in `rayfin.yml` (relative to `root`, or the project root) is wrong, or `buildCommand` didn't actually produce output. **Fix:** verify the `staticHosting.folder` path, and run the build command manually (`npm run build`) to confirm it produces files where expected. ### Deployment too large [#deployment-too-large] **Symptom:** static deployment fails because the packaged archive exceeds the limit. **Cause:** the compressed build output exceeds the 100 MB static hosting limit — often source maps or unoptimized assets. **Fix:** exclude development artifacts (source maps, unminified assets) from the production build, or move large binary files to a storage service instead of bundling them as static content. ### "No remote endpoint configured" [#no-remote-endpoint-configured] **Symptom:** `rayfin up staticapp deploy` fails, saying no remote endpoint is configured. **Cause:** `staticapp deploy` only redeploys static content for a deployment that already exists — it can't create one. **Fix:** run `npx rayfin up` once to provision the remote deployment, then use `npx rayfin up staticapp deploy` for subsequent static-only updates. ## Fabric brokered auth [#fabric-brokered-auth] See [`@microsoft/rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric) for the full option reference these fixes refer to. ### Popup blocked [#popup-blocked] **Cause:** `ensureSignedInWithFabric` (or `initiateFabricLogin`) was called outside a synchronous user-gesture handler. **Fix:** call it directly from a button's `onClick`, not on page load or after an `await`. ### Session not persisting [#session-not-persisting] **Cause:** `RayfinClient` is misconfigured — usually the wrong `baseUrl` or `publishableKey`. **Fix:** confirm both match your deployed backend. ### Timeout after 5 minutes [#timeout-after-5-minutes] **Cause:** the Fabric portal never sent the handoff code back. **Fix:** confirm `returnOrigin` matches your app's actual origin. ### Origin mismatch [#origin-mismatch] **Cause:** `fabricPortalUrl` doesn't match the Fabric portal environment you're actually using (production, PPE, dev). **Fix:** use the correct portal URL for your environment. ### `initEmbeddedAuth` returns `null` [#initembeddedauth-returns-null] **Cause:** the SDK didn't detect embedded mode. **Fix:** ensure the URL includes `?fabricEmbedded=true`, or set `fabricEmbedded: true` explicitly in `FabricAuthOptions`. ### State mismatch error [#state-mismatch-error] **Cause:** the response's state parameter didn't match the request — a stale response from a previous flow, or a replay attempt. **Fix:** retry the sign-in flow from scratch. ## See also [#see-also] * [Known limitations](/docs/reference/known-limitations) * [Deprecation warnings](/docs/reference/deprecations) * [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client) --- --- title: "Reference overview" description: "Landing page for the complete Rayfin reference — CLI commands, configuration schema, SDK packages, rules for coding agents, known limitations, deprecations, and troubleshooting." url: https://rayfin.ai/docs/reference markdown_url: https://rayfin.ai/docs/reference.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T00:07:16-07:00 source: reference/index.mdx --- # Reference overview > Landing page for the complete Rayfin reference — CLI commands, configuration schema, SDK packages, rules for coding agents, known limitations, deprecations, and troubleshooting. This section is exhaustive, precise detail rather than task-first guidance — reach for it when you need an exact flag, option, signature, or error message rather than an explanation of how a feature works. If you're learning a concept for the first time, the [Data](/docs/data), [Auth](/docs/auth), or [Deploy](/docs/deploy) guides are a better starting point. ## In this section [#in-this-section] * **[CLI](/docs/reference/cli)** — every `rayfin` command, subcommand, and flag. * **[Configuration](/docs/reference/config)** — the `rayfin.yml` schema and every environment variable the CLI and runtime read or write. * **[SDK](/docs/reference/sdk)** — API reference for every `@microsoft/rayfin-*` package: decorators, client construction, the query API, and auth. * **[Rules for coding agents](/docs/reference/agent-rules)** — the condensed do/don't list for generating Rayfin code that works. * **[Known limitations](/docs/reference/known-limitations)** — current constraints in the data client, DAB, relationships, auth, and schema apply, each with a workaround. * **[Deprecations](/docs/reference/deprecations)** — how to silence deprecation warnings in application code, Node.js, and browser apps. * **[Errors](/docs/reference/errors)** — a symptom → cause → fix index gathered from every troubleshooting section in the guide. > [!TIP] > Check [Known limitations](/docs/reference/known-limitations) before generating entities, > queries, or permission decorators — several common mistakes (missing `@text()` length, > missing permission decorators, `findByPk` instead of `findById`) are constraints, not > bugs, and are documented there with the correct pattern. ## Package overview [#package-overview] Every package that ships as part of Rayfin, from `catalog.json`: | Package | Kind | What it does | | ------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | [`@microsoft/rayfin-core`](/docs/reference/sdk/rayfin-core) | SDK | Code-first decorators for data model definition. The CLI reads this metadata to generate Data API Builder configuration. | | [`@microsoft/rayfin-data`](/docs/reference/sdk/rayfin-data) | SDK | DAB-compliant data client for GraphQL access patterns against the generated DAB endpoint. | | [`@microsoft/rayfin-auth`](/docs/reference/sdk/rayfin-auth) | SDK | Authentication helpers: session and token management, sign-in flows for Rayfin Builder apps. | | [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client) | SDK | High-level Rayfin client entrypoint composing auth and data APIs behind a single configured client. | | [`@microsoft/rayfin-lib`](/docs/reference/sdk/rayfin-lib) | SDK | Shared HTTP and client utilities used by higher-level Rayfin SDKs. | | [`@microsoft/rayfin-functions`](/docs/reference/sdk/rayfin-functions) | SDK | TypeScript helpers for function-style workflows on top of Rayfin core. | | [`@microsoft/rayfin-storage`](/docs/reference/sdk/rayfin-storage) | SDK | Type-safe blob storage client for Rayfin storage backends. | | [`@microsoft/rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric) | SDK | Microsoft Fabric token provider for Rayfin auth flows. | | [`@microsoft/rayfin-cli`](/docs/reference/cli) | Tool | Command-line interface: scaffolding, configuration, deployment, and the `rayfin docs` command group for offline-friendly agent grounding. | | [`@microsoft/rayfin-mcp`](/docs/reference/cli/docs#mcp-server) | Tool | Model Context Protocol server exposing Rayfin docs as `list_docs`, `search_docs`, `get_doc`, and `discover_packages` tools. | | [`@microsoft/rayfin-docs`](/docs/reference/cli/docs#how-docs-are-discovered) | Tool | Docs indexing/discovery library powering the CLI and MCP server. Merges installed package docs via the `rayfinDocs` `package.json` convention. | | `@microsoft/rayfin-guide` | Guide | Cross-cutting builder guides — getting started, auth overview, data permissions, CLI commands. | | `@microsoft/rayfin-host-docs` | Host | Host service reference docs for Rayfin .NET hosting components (WebService, Auth, Storage, DataApi, Function). | | [`@microsoft/create-rayfin`](/docs/start/quickstart) | Tool | App scaffolding tool for bootstrapping new Rayfin projects via `npm create rayfin@latest`. | ```prompt title="Diagnose a Rayfin deploy error" My rayfin project just failed to deploy with `npx rayfin up` and printed a 400 error whose message includes "Dialect is required when Data module is enabled". Read https://rayfin.ai/docs/reference/errors.md and https://rayfin.ai/docs/reference/known-limitations.md to find the matching entry, then fix my rayfin/rayfin.yml so the data service has an explicit dialect and redeploy. ``` --- --- title: "Known limitations" description: "Current constraints in the Rayfin data client, Data API Builder, relationships, auth, and schema apply — organized by area, each with a workaround." url: https://rayfin.ai/docs/reference/known-limitations markdown_url: https://rayfin.ai/docs/reference/known-limitations.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/known-limitations.mdx --- # Known limitations > Current constraints in the Rayfin data client, Data API Builder, relationships, auth, and schema apply — organized by area, each with a workaround. This page lists the current constraints Rayfin builders run into most often, grouped by area. Each entry names the limitation and the workaround to use instead — if you're an agent generating Rayfin code, check the relevant section here before you write entities, queries, or permission decorators. ## Service availability [#service-availability] ### Functions and storage are experimental [#functions-and-storage-are-experimental] `services.functions` and `services.storage` are experimental services. They are not available in every Fabric region or tenant, and the `@microsoft/rayfin-functions` and `@microsoft/rayfin-storage` packages may change substantially between releases. **Workaround:** deploy a minimal app with the service enabled and confirm `npx rayfin up` succeeds in your target workspace before you design around it. Keep data access in [`client.data.`](/docs/data/querying) where a function is not strictly required. ### Connectors are in private preview [#connectors-are-in-private-preview] [Connectors](/docs/connectors) are behind a feature flag: `rayfin connector` is not registered until the project sets `services.connectors.enabled: true`, has a non-empty `connectors:` block, or runs with `RAYFIN_FEATURE_FLAGS=connectors`. `ConnectorsRayfinClient` ships from the `@microsoft/rayfin-client/experimental` subpath, and the connector packages may change between releases. Connector packages version in lockstep with the CLI, but their npm `latest` and `preview` tags lag the published release. An unversioned install resolves to an older connector that hard-pins its own `@microsoft/rayfin-data`, leaving two Rayfin version lines in one app. **Workaround:** run the version-pinned `npm install` that `rayfin connector add` prints, verbatim, and confirm a connector deploys in your own workspace before designing around it. ### `rayfin connector inspect` does not support Kusto [#rayfin-connector-inspect-does-not-support-kusto] `connector inspect` works for `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`, and `fabric-semanticmodel`. A `kusto` connector fails with `Unsupported connector type: kusto`. **Workaround:** use `npx rayfin connector invoke executeQuery` instead. That transport posts to the deployed item, so it needs a prior `npx rayfin up`. See [KQL databases](/docs/connectors/kusto). ## Data client [#data-client] ### `count()` counts numeric values, not rows [#count-counts-numeric-values-not-rows] The fluent GraphQL client has no `count()` method on the query chain. `count` exists only as an [aggregation](/docs/data/aggregations) operation, and Data API Builder types every aggregation's field argument as the entity's numeric fields — so `count` can only target a numeric column, and returns the number of rows where that column is non-null. **Workaround:** point `count` at a non-nullable numeric column when the entity has one. Otherwise select the minimal set of fields you need and use `results.length`, or page through results with [`.executePaginated()`](/docs/reference/sdk/rayfin-data#pagination) and sum `items.length` per page if the total exceeds one page. ### Grouped aggregation cannot be combined with row selection [#grouped-aggregation-cannot-be-combined-with-row-selection] Data API Builder rejects a query that requests `groupBy` and `items` together, so `.aggregate(...)` is mutually exclusive with `.select()`, `.orderBy()`, `.first()`, and `.after()`. Each combination is a compile-time error backed by a runtime guard. **Workaround:** run the aggregation on its own and sort or slice the returned array in TypeScript. One row per group is normally a small result. ### Many-to-many relationships are not supported [#many-to-many-relationships-are-not-supported] `@one()` and `@many()` model one-to-many and many-to-one relationships only — there is no decorator for many-to-many. **Workaround:** create an explicit join entity with two `@one()` fields, one pointing at each side of the relationship. See [Relationship decorators](/docs/reference/sdk/rayfin-core#relationship-decorators). ## Data API Builder (DAB) [#data-api-builder-dab] Rayfin's data layer generates configuration for Data API Builder, so some limitations come from DAB itself rather than from Rayfin's client or decorators. ### Collection queries return only one page unless you paginate [#collection-queries-return-only-one-page-unless-you-paginate] `.execute()` returns a single page — 100 records by default, up to a maximum page size of 100,000 — with no signal that more records exist. A query that matches more rows than the page size is silently truncated. **Workaround:** use `.first(n)` with `.executePaginated()` and `.after(endCursor)` for any list that can exceed one page. See [Pagination](/docs/reference/sdk/rayfin-data#pagination) and [Errors and troubleshooting](/docs/reference/errors). ### Total record counts are unsupported [#total-record-counts-are-unsupported] There is no way to request a total row count independent of a page ([DAB discussion #2234](https://github.com/Azure/data-api-builder/discussions/2234), [DAB issue #2369](https://github.com/Azure/data-api-builder/issues/2369)) — this is also why `PagedResult.totalCount` exists on the type but is never populated. **Workaround:** don't rely on `totalCount`. If you need an approximate count, select minimal fields and iterate pages with `.executePaginated()`, summing `items.length`. ### Backward pagination (`before`) is unsupported [#backward-pagination-before-is-unsupported] DAB only supports forward pagination — `first` and `after` ([DAB issue #2238](https://github.com/Azure/data-api-builder/issues/2238)). There is no `before` / `last` equivalent on the fluent client. **Workaround:** design paged UI around forward-only navigation (a "next page" cursor kept in state), rather than jumping backward by cursor. ### Nested queries beyond two levels aren't supported [#nested-queries-beyond-two-levels-arent-supported] `.select()` supports one level of relationship dot-path (`'notebook.name'`) but not two (`'notebook.owner.email'`) — the query builder's field-selection logic only splits on the first `.`. **Workaround:** issue a second query for the deeper relationship using the first query's result as the filter, or add a direct field/relationship on the entity that needs the deeper data. ## Relationships [#relationships] ### Foreign key columns are auto-generated [#foreign-key-columns-are-auto-generated] Defining `@one()` or `@many()` already creates the underlying foreign key column — you don't need (and normally shouldn't) declare it yourself. **Workaround:** only declare a foreign key field explicitly when your application code needs to read or set the raw ID value directly. ### Foreign keys must use the `{property}_id` naming convention [#foreign-keys-must-use-the-property_id-naming-convention] When you do declare a foreign key field, it must be named `{property}_id` to match the relationship. Custom key names (`foreignKey`, `targetKey`) are not supported on relationship decorators. **Workaround:** name the field after the relationship property plus `_id` — for a `@one(() => Category) category` field, the FK column is `category_id`. ### `@one()` / `@many()` only accept `{ optional?, unique? }` [#one--many-only-accept--optional-unique-] Relationship decorators don't accept the full field option set (no `default`, `max`, `min`, etc.) — only `optional` (nullable relationship) and `unique` (make a `@one()` relationship one-to-one). **Workaround:** if you need constraints beyond nullability and uniqueness, apply them to the plain field decorator on a manually-declared foreign key column instead of the relationship decorator. ## Auth [#auth] ### The callback is `onSessionChange`, not `onAuthStateChange` [#the-callback-is-onsessionchange-not-onauthstatechange] `onAuthStateChange` does not exist on the Rayfin auth client, despite being a common name in other auth SDKs. **Workaround:** subscribe with `auth.onSessionChange(callback)`, which returns an unsubscribe function. See [`@microsoft/rayfin-auth`](/docs/reference/sdk/rayfin-auth#session-management). ### Session objects are opaque [#session-objects-are-opaque] `OpaqueSession` intentionally doesn't expose tokens or internal claim structure. **Workaround:** gate UI logic on `session.isAuthenticated` or the presence of `session.user` — don't try to read or decode internal session fields. ### Auth config changes require a backend restart [#auth-config-changes-require-a-backend-restart] Enabling or disabling auth (or changing its settings) in `rayfin.yml` doesn't take effect on a running backend. **Workaround:** restart the backend (`npx rayfin up`) after any `services.auth` change in `rayfin.yml` so the updated endpoints are exposed. ## Data [#data] ### `@entity()` doesn't accept composite field constraints [#entity-doesnt-accept-composite-field-constraints] There's no class-level option for constraints that span multiple fields (e.g., "field A must be less than field B"). **Workaround:** define constraints on individual fields via their own field decorator options (`min`, `max`, `regex`, and so on); enforce cross-field constraints in application code or with [`toStandardSchema`](/docs/reference/sdk/rayfin-core#form-validation) before calling `create` / `update`. ### Prefer `@anonymous()` / `@authenticated()` over `@role('anonymous' | 'authenticated', ...)` [#prefer-anonymous--authenticated-over-roleanonymous--authenticated-] The shorthand decorators are clearer at the call site than the general-purpose `@role()` form. **Workaround:** use `@authenticated(actions, options)` for authenticated-only access, and `@anonymous(actions, options)` for public access. Both are exported from the package root. Anonymous access on a deployed Fabric app additionally requires a tenant administrator to enable anonymous data access for the tenant. See [Permission decorators](/docs/reference/sdk/rayfin-core#permission-decorators). ## Database and schema apply [#database-and-schema-apply] ### Run `rayfin up` before `rayfin up db apply` [#run-rayfin-up-before-rayfin-up-db-apply] `rayfin up db apply` expects the backend services it's applying schema to already be running and healthy. **Workaround:** run `npx rayfin up` first and wait for services to report healthy, then run the `db apply` subcommand. ### `unsupported UUID` errors [#unsupported-uuid-errors] This usually means another service is already running on the default port, backed by a different database dialect than your current project expects. **Workaround:** stop the conflicting service (or free the port) and retry. See [Errors and troubleshooting](/docs/reference/errors). ### `dialect` is required when the Data module is enabled [#dialect-is-required-when-the-data-module-is-enabled] Setting `services.data.enabled: true` in `rayfin.yml` without a `dialect` fails at deploy time with `Dialect is required when Data module is enabled`. **Workaround:** always pair `data.enabled: true` with `dialect: mssql` — the only dialect Fabric apps support. See [Errors and troubleshooting](/docs/reference/errors). ### `@text()` without `max` breaks GraphQL schema generation on MSSQL [#text-without-max-breaks-graphql-schema-generation-on-mssql] Omitting `max` on a `@text()` field produces an `NVARCHAR(MAX)` column on MSSQL. Rayfin's metadata provider can fail to build a GraphQL schema from that column, which surfaces as an "Internal server error" at runtime — after `rayfin up` itself reported success. **Workaround:** always set `@text({ max: N })` with an explicit length on every string field targeting MSSQL. See [Errors and troubleshooting](/docs/reference/errors) and the [`@text()` field options](/docs/reference/sdk/rayfin-core) on the `rayfin-core` reference. --- --- title: "Deploy to Fabric" description: "Enable Fabric apps in your tenant, create one in the Fabric portal, and deploy your project with rayfin login and rayfin up." url: https://rayfin.ai/docs/start/deploy-to-fabric markdown_url: https://rayfin.ai/docs/start/deploy-to-fabric.md section: start product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: start/deploy-to-fabric.mdx --- # Deploy to Fabric > Enable Fabric apps in your tenant, create one in the Fabric portal, and deploy your project with rayfin login and rayfin up. Deploying to Fabric provisions your project as a managed **Fabric app** — Fabric hosts the database, the API, authentication, and your built frontend. This page covers the first deployment end to end. ## Prerequisites [#prerequisites] * A Rayfin project with a `rayfin/rayfin.yml` configuration file. * A Microsoft account with access to a Fabric workspace where you have contributor or admin permissions, and Fabric capacity assigned to that workspace. * Fabric apps enabled in your tenant's admin settings (next section). ## Enable Fabric apps in your tenant [#enable-fabric-apps-in-your-tenant] A Fabric tenant administrator must turn this on before anyone can create a Fabric app. If you are not a tenant admin, send this section to whoever is. 1. Sign in to the [Fabric admin portal](https://app.fabric.microsoft.com/admin-portal). 2. Navigate to **Tenant settings**. 3. Under **Fabric Apps (preview)**, toggle the setting to **Enabled**. 4. Choose whether to enable it for the whole organization or specific security groups. 5. Click **Apply**. Changes can take a few minutes to propagate. ## Create a Fabric app [#create-a-fabric-app] You can create the Fabric item first and connect your code to it, or deploy from the CLI and let it create the item for you. **From the Fabric portal:** sign in to [Microsoft Fabric](https://app.fabric.microsoft.com), select or create a workspace, click **New item**, search for **App (preview)**, name it, and click **Create**. Then click **Open in VS Code** on the new item to load the project locally. **From the CLI:** run `rayfin login` followed by `rayfin up` in an existing project (see below) — the first deploy creates the Fabric item in your workspace automatically, and every subsequent deploy reuses it. ## Sign in [#sign-in] ```bash npx rayfin login ``` This opens a browser window for interactive Entra ID sign-in. Tokens are stored securely in the OS keychain under `~/.rayfin/`. Check your status at any time: ```bash npx rayfin login status ``` The MSAL account picker is always shown on sign-in, so if you have several accounts you can choose between them by running `npx rayfin login` again. For non-interactive environments (CI pipelines), authenticate as a service principal instead: ```bash npx rayfin login --service-principal \ --client-id \ --client-secret \ --tenant ``` ## Deploy with `rayfin up` [#deploy-with-rayfin-up] ```bash npx rayfin up ``` If you are not signed in, this launches an interactive login automatically. `rayfin up` is the canonical, all-in-one deploy command — it performs these steps in order: 1. Creates a Rayfin item in your Fabric workspace (or reuses the existing one on later deploys). 2. Retrieves the publishable key from the remote service. 3. Syncs runtime settings from `rayfin.yml` to the remote service — auth configuration and enabled services. 4. Applies the database schema generated from your entity decorators. 5. Builds and deploys static content, if `staticHosting` is enabled — runs your build command, packages the output, and uploads it. 6. Persists deployment details to `rayfin/.deployments.json` and merges the resulting `RAYFIN_PUBLIC_*` values into `rayfin/.env`. When it finishes, the CLI prints the **hosting URL**, a **Fabric portal link**, and the **deployment ID**. Preview what a deploy would do without changing anything: ```bash npx rayfin up -n ``` During local development, skip the static build/deploy phase so a local Vite server keeps serving your frontend while the backend still deploys: ```bash npx rayfin up --exclude-services staticHosting ``` ## Verify the deployment [#verify-the-deployment] ```bash npx rayfin up status ``` Add `--json` for machine-readable output. > [!WARNING] > After adding or changing an entity, confirm the schema actually applied — a deploy can > report success while a newly added entity is not yet readable. Check `npx rayfin up > status`, or query the entity directly, before assuming the change is live. See > [Schema migrations](/docs/data/migrations) for how migrations are generated and applied. ## Applying schema changes after the first deploy [#applying-schema-changes-after-the-first-deploy] For any later change to your entities — a new field, a new entity, a new permission — redeploy with the same command: ```bash npx rayfin up ``` `rayfin up` applies pending schema migrations as part of its normal run, so it is the right command for "deploy this change" even when the change is schema-only. Reach for the narrower subcommand only when you explicitly want to push a schema change without rebuilding or redeploying static content: ```bash npx rayfin up db apply [--force] ``` If the change could cause data loss (dropping a column, changing a type), the CLI blocks it until you add `--force`. ## Authentication [#authentication] Fabric SSO (Entra ID) is the only supported authentication method — make sure it's enabled before you deploy: ```yaml title="rayfin/rayfin.yml" services: auth: enabled: true fabric: enabled: true ``` ## Redeploy static content only [#redeploy-static-content-only] When you have only changed frontend code, skip the rest of the deploy for a faster cycle: ```bash npx rayfin up staticapp deploy ``` Add `--skip-build` to deploy existing build output without rebuilding it. ## Sign out [#sign-out] ```bash npx rayfin logout ``` ## Troubleshooting [#troubleshooting] * **401 or 403 during deploy** — your session expired. Run `npx rayfin login` again, then retry `npx rayfin up`. * **Deploy fails with "Dialect is required"** — `services.data.enabled: true` needs an explicit `dialect: mssql` (Fabric supports MSSQL only). * **Database apply reports destructive changes** — review the listed operations, then add `--force` only once you accept the data loss. * **Static deploy exceeds the size limit** — the compressed archive must stay under 100 MB; exclude source maps and large dev assets, or move binary files to Rayfin storage. * **GraphQL "Internal server error" after a successful deploy** — look for `@text()` fields with no `max`; they generate `NVARCHAR(MAX)` on MSSQL, which can break GraphQL schema generation. Add `@text({ max: N })` and redeploy with `npx rayfin up db apply --force`. See [Deploy troubleshooting](/docs/deploy/troubleshooting) for more. ```prompt title="Deploy my app to Fabric" Deploy my Rayfin app to Microsoft Fabric. Run the workflow yourself rather than printing steps for me to run: 1. `npx rayfin login` (interactive sign-in if I am not already authenticated). 2. `npx rayfin up` to deploy the app and apply pending schema migrations. 3. `npx rayfin up status` to confirm the deployment is healthy. If I have added or changed an entity in this session, confirm after deploying that the change actually applied — a deploy can report success while a newly added entity is not yet readable. If it did not apply, run `npx rayfin up db apply` and tell me what happened. Do not pass `--force` to any command without first showing me what it would change. ``` ## Next [#next] * [Fabric apps](/docs/deploy/fabric-apps) — the Fabric item model in depth. * [Schema migrations](/docs/data/migrations) — how migrations are generated and applied. * [Auth](/docs/auth) — configure Fabric SSO. --- --- title: "Installation" description: "Install Node.js 20+ and the GitHub CLI on Windows, macOS, or Linux, then verify and update the Rayfin CLI." url: https://rayfin.ai/docs/start/installation markdown_url: https://rayfin.ai/docs/start/installation.md section: start product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: start/installation.mdx --- # Installation > Install Node.js 20+ and the GitHub CLI on Windows, macOS, or Linux, then verify and update the Rayfin CLI. Rayfin needs the same prerequisites whether you're scaffolding a new project or deploying to Microsoft Fabric: **Node.js 20 or later** and the **GitHub CLI**. Install these before you scaffold a project. ## Windows [#windows] Install the latest LTS Node.js: ```powershell winget install -e --id OpenJS.NodeJS.LTS ``` Install the GitHub CLI: ```powershell winget install --id GitHub.cli -e ``` Add it to `PATH` if it is not already there: ```powershell where gh # If there are no results, add the GitHub CLI to your PATH: $env:PATH += ";C:\Program Files\GitHub CLI"; [Environment]::SetEnvironmentVariable("PATH", $env:PATH, "User") ``` Sign in and verify every tool: ```powershell gh auth login node --version gh --version ``` > [!NOTE] > If prompted during `gh auth login`, authorize GitHub to access Microsoft. ## macOS [#macos] ```bash brew install node@lts brew install gh ``` Verify the GitHub CLI is on `PATH`, sign in, and check versions: ```bash which gh gh auth login node --version gh --version ``` > [!NOTE] > If prompted during `gh auth login`, authorize GitHub to access Microsoft. ## Linux (Ubuntu or Debian) [#linux-ubuntu-or-debian] Install the latest LTS Node.js using the [official Node.js download instructions](https://nodejs.org/en/download). Install the GitHub CLI: ```bash type -p curl >/dev/null || sudo apt install -y curl curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null sudo apt update sudo apt install -y gh ``` Verify the GitHub CLI is on `PATH`, sign in, and check versions: ```bash which gh gh auth login node --version gh --version ``` ## Install the Rayfin CLI [#install-the-rayfin-cli] You do not usually install `@microsoft/rayfin-cli` directly — scaffolding a project installs it for you as a dev dependency. **New project:** ```bash npm create @microsoft/rayfin@latest my-app cd my-app ``` **Existing project** — install the CLI, then run the interactive setup to create the `rayfin/` directory with starter configuration files: ```bash npm install --save-dev @microsoft/rayfin-cli npx rayfin init ``` ## Verify the installation [#verify-the-installation] ```bash npx rayfin --version ``` List every available command: ```bash npx rayfin --help ``` ## Update the CLI [#update-the-cli] ```bash npm update --save npm install ``` Confirm the update: ```bash npx rayfin --version ``` ```prompt title="Install Rayfin's prerequisites" Check whether this machine has Node.js 20 or later and the GitHub CLI installed, and install or update whichever are missing for this operating system. Then run `gh auth login` to sign in, and print the output of `node --version` and `gh --version` so I can confirm everything is ready before I scaffold a Rayfin project. ``` ## Next [#next] * [Quickstart](/docs/start/quickstart) — scaffold your first project. * [Project structure](/docs/start/project-structure) — what the scaffold generates. * [CLI installation reference](/docs/reference/cli/installation) — every install-related command and flag. --- --- title: "Project structure" description: "The rayfin/ folder layout — rayfin.yml, entities under rayfin/data/, schema.ts, generated files, and how the frontend picks up backend config." url: https://rayfin.ai/docs/start/project-structure markdown_url: https://rayfin.ai/docs/start/project-structure.md section: start product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: start/project-structure.mdx --- # Project structure > The rayfin/ folder layout — rayfin.yml, entities under rayfin/data/, schema.ts, generated files, and how the frontend picks up backend config. Every template scaffolded with `npm create @microsoft/rayfin@latest` follows the same layout, so data models, backend configuration, and frontend code stay in predictable places. ## Folder layout [#folder-layout] ```text my-app/ ├── rayfin/ │ ├── data/ │ │ ├── schema.ts │ │ └── Todo.ts │ ├── .temp/ │ ├── .env │ ├── .deployments.json │ ├── rayfin.yml │ └── tsconfig.json ├── src/ ├── package.json ├── tsconfig.json └── README.md ``` ## rayfin/rayfin.yml [#rayfinrayfinyml] The entry point for your backend configuration. It controls which services `rayfin up` starts (or deploys), and its string values support `${VAR}` / `${VAR:-default}` interpolation from `rayfin/.env` — see [Environment variable interpolation](/docs/reference/config/env-interpolation). ```yaml title="rayfin/rayfin.yml" id: my-app name: my-app version: 1.0.0 services: auth: enabled: true allowedRedirectUris: - http://localhost:5173 fabric: enabled: true data: enabled: true dialect: mssql storage: enabled: false staticHosting: enabled: true root: . folder: dist buildCommand: npm run build indexDocument: index.html ``` | Field | Description | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Project slug — used as the Fabric item identifier. | | `name` | Human-readable project display name. | | `version` | Project version (semver). | | `services.auth` | Enables sign-in and configures redirect URIs and Fabric SSO. See [Auth](/docs/auth). | | `services.data` | Enables the data service and its `dialect` (`mssql`). See [Data](/docs/data). | | `services.storage` | Enables blob storage. | | `services.staticHosting` | Enables building and hosting your frontend, including the `buildCommand` `rayfin up` runs before packaging it. See [Hosting](/docs/hosting). | Declare `services.auth` and `services.data` explicitly, even as `enabled: false` — the CLI reads those keys directly and does not fill in a default when they are missing entirely. The full field reference is in [`rayfin.yml`](/docs/reference/config/rayfin-yml). ## rayfin/data/ and schema.ts [#rayfindata-and-schemats] Files in `rayfin/data/` define your entities — TypeScript classes decorated with `@entity()` plus one field decorator per property: ```typescript title="rayfin/data/Todo.ts" import { entity, authenticated, uuid, text, boolean } from '@microsoft/rayfin-core'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id) }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @boolean({ default: false }) done!: boolean; @text({ max: 128 }) user_id!: string; } ``` `rayfin/data/schema.ts` maps entity names to their classes. The Rayfin client uses this map to provide type-safe access to `client.data.`: ```typescript title="rayfin/data/schema.ts" import { Todo } from './Todo.js'; export type AppSchema = { Todo: Todo; }; export const schema = [Todo]; ``` Register every entity file here — an entity that exists in `rayfin/data/` but is missing from `schema.ts` is not part of your typed client. See [Modeling entities](/docs/data/modeling) for field types, relationships, and permissions. ## rayfin/.env [#rayfinenv] An optional environment file that supplies values to `rayfin.yml` via interpolation, and the file `rayfin up` writes generated deployment values into — the `RAYFIN_PUBLIC_*` variables your frontend reads, plus the Fabric item and workspace IDs. It is gitignored — commit a `rayfin/.env.example` instead to document the variables a teammate needs to fill in. See [Environment variables](/docs/reference/config/environment-variables) for the full list. ## rayfin/.deployments.json [#rayfindeploymentsjson] Written after your first `npx rayfin up` deploy to Fabric. It is a per-workspace registry of deployment metadata (`fabricItemId`, `hostingUrl`, `publishableKey`, and more) so repeated deploys update the same Fabric item instead of creating a new one. Gitignored — see [Deploy to Fabric](/docs/start/deploy-to-fabric). ## rayfin/tsconfig.json and the root tsconfig.json [#rayfintsconfigjson-and-the-root-tsconfigjson] `rayfin/tsconfig.json` is a project-reference config the CLI uses to compile your entity definitions. It extends your root `tsconfig.json` and overrides what it needs (for example, `composite: true`). You should not need to edit it. Your root `tsconfig.json` needs a project reference to `rayfin/`, plus the decorator-related compiler options Rayfin's TC39 Stage 3 decorators require: ```json title="tsconfig.json" { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable", "ESNext.Decorators"], "module": "ESNext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["src"], "references": [{ "path": "./rayfin" }] } ``` > [!NOTE] > Do not set `emitDecoratorMetadata` to `true`. TypeScript only allows it alongside > `experimentalDecorators`, which is incompatible with Rayfin's TC39 decorators. Templates created with `npm create @microsoft/rayfin@latest` already include these settings. If you are integrating Rayfin into an existing project, check your `tsconfig.json` against them. ## rayfin/.temp/ (generated) [#rayfintemp-generated] Generated backend artifacts — the compiled entity output and the Data API Builder configuration used to apply your schema to the deployed Fabric backend. If the backend seems to be using stale schema or configuration, rerun `npx rayfin up` to regenerate this folder and reapply it. ## Frontend wiring [#frontend-wiring] ### Vite configuration [#vite-configuration] Rayfin's decorators require an ES2022 (or later) compilation target. Set `target: 'es2022'` in all three places Vite reads it: ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], build: { target: 'es2022', }, esbuild: { target: 'es2022', }, optimizeDeps: { esbuildOptions: { target: 'es2022', }, }, }); ``` > [!WARNING] > Use `@vitejs/plugin-react` (esbuild-based), not `@vitejs/plugin-react-swc`. The SWC > plugin only supports legacy/experimental decorators and fails to parse Rayfin's TC39 > decorators with an `Expression expected` error, regardless of the `target` setting. ### Environment variables and the predev/prebuild hooks [#environment-variables-and-the-predevprebuild-hooks] Rayfin writes runtime values to `rayfin/.env` using the `RAYFIN_PUBLIC_*` prefix. Your frontend never reads that file directly — instead, the scaffolded `predev` and `prebuild` npm scripts call `rayfin env` to generate a framework-specific `.env.local`: ```json title="package.json" { "scripts": { "predev": "rayfin env --framework vite", "prebuild": "rayfin env --framework vite", "dev": "vite", "build": "tsc -b && vite build" } } ``` When the CLI detects a Vite or Next.js project automatically, you can omit `--framework`. For Vite, `RAYFIN_PUBLIC_API_URL` becomes `VITE_RAYFIN_API_URL` and `RAYFIN_PUBLIC_PUBLISHABLE_KEY` becomes `VITE_RAYFIN_PUBLISHABLE_KEY` in `.env.local`. To change a value, edit `rayfin/.env` and re-run `npm run dev` (or `rayfin env --framework vite` directly) to regenerate it. ## Next [#next] * [Quickstart](/docs/start/quickstart) — scaffold a project with this layout. * [Deploy to Fabric](/docs/start/deploy-to-fabric) — deploy and iterate on the backend. * [Modeling entities](/docs/data/modeling) — the full entity and decorator reference. --- --- title: "Quickstart" description: "Scaffold a Rayfin project, deploy it to Microsoft Fabric, and run the frontend locally — from nothing to a running app in four commands." url: https://rayfin.ai/docs/start/quickstart markdown_url: https://rayfin.ai/docs/start/quickstart.md section: start product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T00:07:16-07:00 source: start/quickstart.mdx --- # Quickstart > Scaffold a Rayfin project, deploy it to Microsoft Fabric, and run the frontend locally — from nothing to a running app in four commands. From nothing to a running app in four commands. You need [Node.js 20 or later and the GitHub CLI](/docs/start/installation) first. Rayfin deploys your backend to Microsoft Fabric and you run your frontend locally against it. There is no backend to run yourself. ```prompt title="Let an agent do all of this" Set up a new Rayfin app for me, end to end. Rayfin is a backend platform for TypeScript developers on Microsoft Fabric. Before writing any code, read https://rayfin.ai/docs/reference/agent-rules.md — every page on that site is available as raw Markdown by appending .md to its URL. Then do the work yourself rather than printing steps for me: 1. Scaffold a project with `npm create @microsoft/rayfin@latest my-app` and install dependencies. 2. Sign in with `npx rayfin login`. 3. Deploy with `npx rayfin up` and confirm it with `npx rayfin up status`. 4. Start the frontend with `npm run dev` and tell me the URL to open. ``` ## 1. Scaffold a project [#1-scaffold-a-project] ```bash npm create @microsoft/rayfin@latest my-app cd my-app ``` Pick a template when prompted. You should see `✔ Project created`. ## 2. Sign in [#2-sign-in] ```bash npx rayfin login ``` Opens a browser for Entra ID sign-in. You need an account with access to a Fabric workspace. ## 3. Deploy [#3-deploy] ```bash npx rayfin up ``` This creates the Fabric item, applies the database schema generated from your entities, and deploys your frontend. Confirm it worked: ```bash npx rayfin up status ``` > [!TIP] > While iterating on frontend code, add `--exclude-services staticHosting` to skip > rebuilding and uploading the bundle — the local dev server serves it instead. ## 4. Run the frontend [#4-run-the-frontend] ```bash npm run dev ``` The scaffolded `predev` script runs `rayfin env --framework vite`, which writes a `.env.local` pointing at the backend you just deployed. Open the URL it prints and you have a working app reading and writing real data. ## Make your first change [#make-your-first-change] Entities live in `rayfin/data/`. Add a field: ```typescript title="rayfin/data/Timestamp.ts" import { entity, anonymous, uuid, text, date } from '@microsoft/rayfin-core'; @entity() @anonymous() export class Timestamp { @uuid() id!: string; @date() timestamp!: Date; @text({ max: 500 }) message!: string; } ``` Then redeploy with the same command: ```bash npx rayfin up ``` `rayfin up` applies pending schema changes as part of a normal deploy. Two rules worth knowing now: every `@text()` field needs a `max`, and every entity needs an explicit permission decorator. ## Next [#next] * [Project structure](/docs/start/project-structure) — what got generated and why. * [Modeling entities](/docs/data/modeling) — how classes become tables and APIs. * [Schema migrations](/docs/data/migrations) — how changes reach the database. * [Known limitations](/docs/reference/known-limitations) — read before modeling seriously. --- --- title: "Storage" description: "What's documented so far about Rayfin's blob storage — the @blob() decorator, storage permissions, and the storage service flag in rayfin.yml." url: https://rayfin.ai/docs/storage markdown_url: https://rayfin.ai/docs/storage.md section: storage product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: storage/index.mdx --- # Storage > What's documented so far about Rayfin's blob storage — the @blob() decorator, storage permissions, and the storage service flag in rayfin.yml. `@microsoft/rayfin-storage` is Rayfin's type-safe blob storage client. This page covers only what's currently verifiable from the Rayfin decorator reference and the permissions guide — it does not attempt to document the storage client's full API surface. > [!WARNING] > Storage is experimental and is not available in every Fabric region or tenant. > `@microsoft/rayfin-storage` may change substantially between releases. Confirm the > service deploys in your own workspace before you design an app around it. > [!NOTE] > This page documents the `@blob()` decorator, storage permissions, and the `storage` > service flag — that's the extent of what's currently verifiable. For the storage client's > actual method signatures (uploading, reading, listing, and so on), run > `rayfin docs search 'storage'` from your project root, or use the > [MCP server](/docs/reference/cli/docs#mcp-server), so you get the API for the version of > `@microsoft/rayfin-storage` actually installed rather than a guess. ## Enable storage [#enable-storage] ```yaml title="rayfin/rayfin.yml" services: storage: enabled: true ``` ## Marking a class as blob storage: `@blob()` [#marking-a-class-as-blob-storage-blob] `@blob()` is a class-level decorator that marks a class as a storage folder configuration — the storage equivalent of `@entity()` for data models. It's structurally different from a data entity, though: it takes an optional folder-name string instead of field options, and the properties inside the class are plain TypeScript fields rather than `@text()` / `@uuid()` decorated columns. ```typescript title="rayfin/storage/ProfileImage.ts" import { blob } from '@microsoft/rayfin-core'; @blob('uploads') export class ProfileImage { owner_id!: string; } ``` The string argument (`'uploads'` above) is the storage folder name; it defaults to the kebab-case class name if omitted. `@blob()` is exported from `@microsoft/rayfin-core`, alongside the data-model decorators — it isn't part of `@microsoft/rayfin-storage` itself. See [Field types](/docs/data/field-types#blob-storage-folders) for the full decorator reference. ## Storage permissions [#storage-permissions] The same `@role()` / `@anonymous()` / `@authenticated()` decorators used for data permissions work on `@blob()` classes. Applied to a blob entity, Rayfin generates a storage policy instead of a database policy, using the same `policy` / `include` / `exclude` options: ```typescript title="rayfin/storage/ProfileImage.ts" import { blob, authenticated } from '@microsoft/rayfin-core'; @blob('uploads') @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.owner_id), }) export class ProfileImage { owner_id!: string; } ``` This restricts every action (`'*'`) on `ProfileImage` blobs to the authenticated user whose `sub` claim matches the blob's `owner_id`. See [Permissions](/docs/data/permissions) for the full reference — the decorator, its `policy` / `include` / `exclude` options, and how policies compile down — all of which applies the same way to storage entities as it does to data entities. ## What isn't covered here [#what-isnt-covered-here] File upload, download, and listing operations are part of the `@microsoft/rayfin-storage` client, not the decorators above. That client's method signatures aren't verifiable from the sources this site is built from — see the note at the top of this page for how to look them up against the version actually installed in your project. ```prompt title="Look up the current storage API before using it" I want to use @microsoft/rayfin-storage in my Rayfin project. Before writing any code, run `rayfin docs search 'storage'` (or the MCP server's search_docs tool with module: 'ts-sdk') from the project root to find the actual client API for the version installed here. Show me what it returns, then use that — not a guess — to implement the upload/read logic I need. ``` --- --- title: "Ai-files" description: "rayfin init ai-files install and status manage AGENTS.md, .mcp.json, and the Rayfin skill — flags, conflict resolution, exit codes, and the drift nudge in rayfin up." url: https://rayfin.ai/docs/reference/cli/ai-files markdown_url: https://rayfin.ai/docs/reference/cli/ai-files.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/ai-files.mdx --- # Ai-files > rayfin init ai-files install and status manage AGENTS.md, .mcp.json, and the Rayfin skill — flags, conflict resolution, exit codes, and the drift nudge in rayfin up. `rayfin init ai-files` installs and refreshes the three agent context files a Rayfin project ships: * **`AGENTS.md`** — project-level instructions for coding agents (scope, conventions, commands to build/test/lint). A one-time install: the CLI seeds a default on first scaffold and never overwrites it afterward, so your edits stick. * **The `mcpServers.rayfin` key in `.mcp.json`** — wires up the Rayfin MCP server so connected agents can query version-locked Rayfin documentation as structured tool calls instead of shelling out to `rayfin docs`. See [Docs](/docs/reference/cli/docs#mcp-server). * **`.agents/skills/rayfin/SKILL.md`** — the Rayfin skill: guidance an agent loads to know which docs tools to call, common Rayfin patterns, and project conventions. This page is the command reference for keeping those three files installed and up to date. Scaffolding with `npm create @microsoft/rayfin@latest` or `rayfin init` installs these automatically — you typically only run this by hand to refresh an existing project after upgrading the CLI, or to add agent files to a project that predates them. ## `ai-files install` [#ai-files-install] ```bash npx rayfin init ai-files install ``` Idempotent: re-running reconciles the project to the bundled content for your current CLI version. Nothing is written when the project is already up to date. | Flag | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-f, --force [ids...]` | Overwrite items that were hand-edited (`user-modified`) or restore ones that were deleted (`missing`). With no arguments, applies to every managed item; pass one or more namespaced IDs (e.g. `--force mcp:rayfin`) to scope it. Never overwrites `AGENTS.md`. | | `-y, --yes` | Skip the interactive prompt and accept defaults (alias of `--non-interactive`). | | `--non-interactive` | Skip the interactive prompt and accept defaults (all items enabled). | | `--enable ` | Install or keep a specific item by its namespaced ID (e.g. `skill:rayfin`). Repeatable. | | `--disable ` | Stop managing a specific item — records the choice without deleting the on-disk file. Repeatable. Also accepts orphaned IDs the lockfile remembers but the current CLI no longer ships. | | `--remove-files` | Modifier for `--disable` that also deletes the on-disk file. Cannot be passed alone. | | `--json` | Emit a `{status, schemaVersion, dryRun, report}` envelope instead of human-readable progress lines. Implies non-interactive. | | `-n, --dry-run` | Classify what `install` would do and emit the report, without writing to disk. Pairs with `--json`. | ### Exit codes [#exit-codes] | Code | Meaning | | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | Success, no warnings. | | `1` | Hard error — invalid arguments, unknown ID, malformed lockfile, or an unrecovered write failure. | | `3` | Success with warnings (for example, `user-modified` items were preserved rather than overwritten). Distinct from `1` so scripted consumers can tell "you should look at this" apart from "the command failed." | ## `ai-files status` [#ai-files-status] ```bash npx rayfin init ai-files status ``` Prints one line per managed item with its current state. Add `--json` for `{status, schemaVersion, items: ItemStatus[]}`. | Flag | Description | | -------- | ---------------------------------------------- | | `--json` | Emit a JSON object instead of formatted lines. | ### Item states [#item-states] | State | Meaning | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `up-to-date` | On disk; matches the lockfile and the bundled content. Nothing to do. | | `update-available` | On disk and matches the lockfile, but the CLI now ships different content. `install` refreshes it. | | `user-modified` | On disk but its hash doesn't match what the CLI last wrote — treated as an intentional edit. `install --force ` overwrites it; `install --disable ` keeps your version. | | `missing` | Previously installed, but the file is gone. `install --force ` reinstalls it. | | `not-installed` | Known to the CLI but not yet installed. Plain `install` installs it. | | `disabled` | Opted out via `--disable `, or (for the skill) by removing the `rayfin-managed: true` frontmatter sigil. `install --enable ` re-enables. | | `orphaned` | The lockfile remembers an item the current CLI no longer ships. `install` cleans it up if untouched, or warns if you edited it. | | `unreadable` | On disk but malformed (e.g. invalid JSON in `.mcp.json`). Repair by hand, or `install --force ` to rebuild it. | ## Conflict resolution [#conflict-resolution] The CLI tracks what it last wrote in `rayfin/.lockfile.json` — commit it so your team shares the same baseline. Re-running `install` compares the on-disk content against that lockfile and against the bundled content for your current CLI version: | State | Default | With `--force` | | -------------------------------- | --------------------------- | ------------------------------------------------------------ | | `not-installed` | install | install | | `up-to-date` | no-op | no-op | | `update-available` | rewrite | rewrite | | `user-modified` | warn, preserve your content | overwrite with bundled | | `missing` | warn | reinstall | | `disabled` (via `--disable`) | skip | skip — `--force` alone won't re-enable; pass `--enable ` | | `disabled` (skill sigil removed) | skip | skip — pass `--enable --force` to re-stamp the sigil | | `unreadable` | warn | overwrite (rebuild from scratch) | | `orphaned`, untouched | delete (lockfile + disk) | delete | | `orphaned`, edited | warn, preserve | delete | `AGENTS.md` is the one exception to all of this: it's a **one-time install**. If it already exists — written by the CLI, your template, or you by hand — `install` never overwrites it, even with `--force`. ## Drift nudge [#drift-nudge] `rayfin up` prints a one-line nudge at startup if any managed item is out of date, modified, missing, or newly shipped: ```text ℹ️ Your project's Rayfin agent files have changes available. Run `rayfin init ai-files install` to refresh. ``` It's content-based, not version-based — upgrading the CLI to a version that ships identical content produces no nudge. ## Scripting and CI [#scripting-and-ci] ```bash # Check what would change without writing npx rayfin init ai-files install -n --json # Idempotent install in CI — warnings exit 3, hard errors exit 1 npx rayfin init ai-files install --yes --json # Inspect current state without writes npx rayfin init ai-files status --json ``` ```prompt title="Refresh agent files after a CLI upgrade" Check whether this project's Rayfin agent files have drifted with `npx rayfin init ai-files status --json`, then run `npx rayfin init ai-files install --yes --json` to refresh anything out of date. Tell me if any item came back user-modified and needs a manual decision. ``` --- --- title: "Connector" description: "rayfin connector manages external Fabric sources, from discovery and registration through inspection and invocation." url: https://rayfin.ai/docs/reference/cli/connector markdown_url: https://rayfin.ai/docs/reference/cli/connector.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/cli/connector.mdx --- # Connector > rayfin connector manages external Fabric sources, from discovery and registration through inspection and invocation. > [!NOTE] > `connector` is registered only when connectors are enabled. Enable it with > `services.connectors.enabled: true` in `rayfin/rayfin.yml`, with any non-empty > top-level `connectors:` block, or for one command with > `RAYFIN_FEATURE_FLAGS=connectors`. Without one of those opt-ins, the CLI reports > `connector` as an unknown command. Use `rayfin connector` to discover existing Microsoft Fabric sources, register them in `rayfin.yml`, inspect their read shape, and invoke configured operations. For the workflow and app-code model, start with [Connectors](/docs/connectors); this page is the command reference. ```bash npx rayfin connector ``` ## Connector types [#connector-types] | Type | Fabric item | Category | Operations | Version | | ---------------------- | -------------- | ---------- | ------------------------------------ | ---------------------------------- | | `fabric-sqlanalytics` | Lakehouse | Category A | `read` | No | | `fabric-warehouse` | Warehouse | Category A | `read`, `create`, `update`, `delete` | No | | `fabric-sqldatabase` | SQL Database | Category A | `read`, `create`, `update`, `delete` | No | | `fabric-semanticmodel` | Semantic model | Category B | `executeQuery` | Pinned to `'1'` by `connector add` | | `kusto` | KQL Database | Category B | `executeQuery`, `executeCommand` | Pinned to `'1'` by `connector add` | Category A connectors generate entity configuration for Data API Builder. Category B connectors expose function-style query operations and are delegated-only. See [Connectors](/docs/connectors), [Fabric SQL sources](/docs/connectors/sql-sources), [Semantic models](/docs/connectors/semantic-models), and [KQL databases](/docs/connectors/kusto). ## Subcommands [#subcommands] The registered subcommands are `search`, `add`, `list`, `remove`, `types`, `inspect`, and `invoke`. ## `connector search` [#connector-search] ```bash npx rayfin connector search [query] --workspace-id --type npx rayfin connector search [query] --all-workspaces --type npx rayfin connector search [query] ``` Searches Fabric items that the signed-in identity can add as connectors. Use one of these scopes: | Scope | Required flags | Behavior | | ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------- | | Single workspace | `--workspace-id ` and `--type ` | Searches one workspace. | | Tenant-wide | `--all-workspaces` and `--type ` | Searches all accessible workspaces. | | Deployed project | No scope flags | Searches every workspace recorded in the project's deployments; `--type` is optional. | | Flag | Description | | ----------------------------------- | ---------------------------------------------------------------------------------------------- | | `[query]` | Optional case-insensitive item-name filter. | | `--query ` | Same filter as `[query]`; wins when both are provided. | | `--workspace-id ` | Search one Fabric workspace. Requires `--type`. | | `--all-workspaces` | Search every workspace the signed-in identity can access. Requires `--type`. | | `--type ` | Narrow to one or more comma-separated connector types. Required with explicit workspace scope. | | `--limit ` | Limit non-interactive output to the first `n` results. | | `--output interactive\|plain\|json` | Select output mode. | | `--json` | Emit one JSON object and skip the interactive picker. | | `-v, --verbose` | Enable verbose output. Cannot be combined with `--json`. | | `-y, --yes` | Auto-accept prompts in the interactive add handoff. | With `--json`, the command emits `{ status, query, scope, count, sources }`. Each result in `sources` includes a suggested name and a ready-to-run `addCommand`. ## `connector add` [#connector-add] ```bash npx rayfin connector add --type fabric-sqldatabase --workspace-id --item-id --name sales ``` Registers one source in `rayfin/rayfin.yml` and scaffolds `rayfin/connectors//`. | Flag | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `--type ` | Required. One of the five connector type literals. | | `--workspace-id ` | Required Fabric workspace ID. Must be a literal value; `${VAR}` placeholders are rejected. | | `--item-id ` | Required Fabric item ID. Must be a literal value; `${VAR}` placeholders are rejected. | | `--name ` | Connector name. If omitted, the CLI derives one from the Fabric item display name. Must match `/^[a-zA-Z0-9\-_]+$/` and be at most 256 characters. | | `--operations ` | Comma-separated subset of allowed operations. This can narrow the catalog defaults but cannot widen them; there is no `all` meta-operation. | | `-y, --yes` | Auto-accept confirmation prompts, including overwriting an existing connector. | | `-v, --verbose` | Enable verbose output. | | `--json` | Emit machine-readable JSON. | The command verifies the Fabric item, writes the `connectors:` entry, and writes `rayfin/connectors//schema.ts`. For Category A it also runs schema discovery and writes `metadata.json`. For `kusto`, it resolves `queryServiceUri` and `databaseName` and bakes them into `schema.ts`; those values are not written to `rayfin.yml`. For versioned Category B types (`fabric-semanticmodel` and `kusto`), `connector add` writes the catalog `defaultVersion` automatically. Today that value is `'1'`. `connector add` does not install packages. It prints a version-pinned `npm install` command; with `--json`, the same data appears under `install.packages` and `install.command`. ## `connector list` [#connector-list] ```bash npx rayfin connector list ``` Lists connectors declared in `rayfin.yml`. It does not call Fabric or the deployed workload. | Flag | Description | | --------------- | ---------------------------------------------- | | `-v, --verbose` | Include catalog metadata for each connector. | | `--json` | Emit the configured connector entries as JSON. | ## `connector remove` [#connector-remove] ```bash npx rayfin connector remove sales --yes ``` Removes the named connector entry from `rayfin.yml` and deletes `rayfin/connectors//`. | Flag | Description | | --------------- | ----------------------------------------------------------------------------------------------- | | `-y, --yes` | Auto-accept confirmation prompts. Required in non-interactive contexts for existing connectors. | | `-v, --verbose` | Enable verbose output. | | `--json` | Emit machine-readable JSON. | JSON output for a removed connector is `{ status, action: 'connector.remove', name, removed, directoryDeleted }`. ## `connector types` [#connector-types-1] ```bash npx rayfin connector types --verbose ``` Lists the connector catalog. | Flag | Description | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `-v, --verbose` | Add category, default auth, dialect, operations, discoverable item types, required version, and client packages. | | `--output interactive\|plain\|json` | Select output mode. | | `--json` | Emit the catalog as JSON. | `--json` emits the same detailed catalog fields as verbose mode. ## `connector inspect` [#connector-inspect] ```bash npx rayfin connector inspect --name sales npx rayfin connector inspect --name sales --entity dbo.Customers --rows 10 npx rayfin connector inspect --workspace-id --item-id --type fabric-warehouse --query queries\sample.sql ``` Inspects a source in read-only mode. It supports `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`, and `fabric-semanticmodel`. It does not support `kusto`; that type fails with `Unsupported connector type: kusto`. | Flag | Description | | -------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `--name ` | Select a connector declared in `rayfin.yml`. Mutually exclusive with direct selectors. | | `--workspace-id ` / `--workspace ` | Direct mode workspace selector. Use with `--item-id` or `--item` and `--type`. | | `--item-id ` / `--item ` | Direct mode item selector. Use with a workspace selector and `--type`. | | `--type ` | Connector type for direct mode. | | `--url ` | Semantic-model portal URL; extracts workspace and item IDs. Cannot be combined with other selectors. | | `--entity ` | Structured entity sampling mode. | | `--query ` | Raw query mode. Path must point to a `.sql` or `.dax` file inside the project. | | `--rows ` | Row cap. Default is 100 for entity listing and 10 for entity or query modes; maximum is 100. | | `-v, --verbose` | Enable verbose output. | | `--output interactive\|plain\|json` | Select output mode. | | `--json` | Emit a machine-readable JSON object. | Inspect has three modes: | Mode | How to select it | Query shape | | ----------------- | ----------------------------- | -------------------------------------------------------------------------------------- | | Entity listing | Omit `--entity` and `--query` | SQL uses `INFORMATION_SCHEMA.TABLES`; DAX uses `INFO.TABLES()`. | | Structured entity | Pass `--entity ` | SQL builds `SELECT TOP(n) * FROM `; DAX builds `EVALUATE TOPN(n, '')`. | | Raw query | Pass `--query ` | Runs the `.sql` or `.dax` file verbatim. | SQL inspect accepts only read queries that start with `SELECT` or `WITH`; DML and DDL are rejected. DAX inspect requires the query to start with `EVALUATE`. ## `connector invoke` [#connector-invoke] ```bash npx rayfin connector invoke mymodel executeQuery --input '{"query":"EVALUATE TOPN(10, Sales)"}' npx rayfin connector invoke --name mymodel --operation executeQuery --file payload.json ``` Invokes one configured operation. The positional connector name and operation also have flag forms, `--name` and `--operation`; flag values win over positionals. | Flag | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `` | Connector name declared in `rayfin.yml`. | | `` | Operation to invoke. | | `--name ` | Connector name flag form; wins over the positional. | | `--operation ` | Operation flag form; wins over the positional. | | `--input ''` | Inline JSON payload. Exactly one of `--input` or `--file` is required. | | `--file ` | JSON payload file, resolved against the project root. The resolved path must stay inside it; `..\` escapes are rejected before the file is read. | | `-v, --verbose` | Enable verbose output. Cannot be combined with `--json`. | | `--output interactive\|plain\|json` | Select output mode. | | `--json` | Emit one machine-readable JSON object. | Operation names match case-insensitively against the connector's `operations:` list. If the entry has no operations, the command falls back to the connector type's full catalog allowlist. ### Invoke transports [#invoke-transports] `fabric-semanticmodel` calls Fabric and Power BI directly under the developer's own identity. It works with or without `rayfin up`, and it requires `workspaceId` and `itemId` under the connector's `config:` block. Every other type, including `kusto`, POSTs to the deployed item at `/__private/connectors//invoke` and requires a prior `rayfin up`. ### Invoke token handling [#invoke-token-handling] `rayfin login` consents to the Fabric scope, not the Power BI scope the semantic-model path needs. Interactively, the CLI prompts to complete Power BI consent. With `--json`, token acquisition is silent-only so prompts cannot corrupt the single-JSON-object contract; if consent is still needed, the command fails and tells you to drop `--json` or set `RAYFIN_TOKEN`. When `RAYFIN_TOKEN` is set, it is passed through unchanged and its audience is decoded and checked locally. ### Invoke output [#invoke-output] Success emits `{ status: 'ok', connector, operation, output }`. A resolved call is not automatically a success: a normalizing connector reports failure as `status: 'error'`, and a connector returning a raw envelope reports failure as `status: 'Failed'`. The CLI converts either failure shape into a non-zero exit. > [!NOTE] > Invoking a semantic model operation whose DAX returns Int64 columns, for example > `DISTINCTCOUNT`, can fail while printing with `Do not know how to serialize a BigInt` even > though the query succeeded. Select a non-Int64 column to read the output. ### Invoke errors [#invoke-errors] | Message | Fix | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `Missing required connector invoke arguments` | Supply both the connector name and the operation. | | `Choose exactly one payload source` / `Missing payload input` | Pass exactly one of `--input` or `--file`. | | `Input file must be inside the project` | Use a relative path under the project root. | | `Operation "" is not allowed for connector ""` | Check the connector's `operations:` in `rayfin.yml`, or the type's allowlist. | | `missing workspaceId/itemId in rayfin.yml` | Add both under `config:`, or re-run [`connector add`](#connector-add). | | `Access token has the wrong audience for the Power BI query API` | Unset or replace `RAYFIN_TOKEN`, or re-run without `--json` to consent interactively. | | `No remote endpoint configured` | The non-semantic-model transport needs a deployed item; run `npx rayfin up` first. | ```prompt title="Register a Fabric SQL Database connector" Add a Rayfin connector named "sales" for my Fabric SQL Database using `npx rayfin connector add --type fabric-sqldatabase --workspace-id --item-id --name sales`, then show me the resulting rayfin.yml entry and the scaffolded rayfin/connectors/sales/schema.ts. ``` --- --- title: "Docs" description: "rayfin docs search, get, list, discover, and catalog show — query version-locked Rayfin documentation from the terminal when the MCP server isn't available." url: https://rayfin.ai/docs/reference/cli/docs markdown_url: https://rayfin.ai/docs/reference/cli/docs.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T00:07:16-07:00 source: reference/cli/docs.mdx --- # Docs > rayfin docs search, get, list, discover, and catalog show — query version-locked Rayfin documentation from the terminal when the MCP server isn't available. `rayfin docs` queries the same documentation corpus and search index as the [Rayfin MCP server](#mcp-server), as a CLI surface for offline use, scripting, and agents without MCP access. Run it from your project root so it resolves documentation from *that project's* installed packages. ## How docs are discovered [#how-docs-are-discovered] Rayfin packages declare a `rayfinDocs` field in their `package.json`, naming the package and pointing at a bundled directory of Markdown: ```json title="node_modules/@microsoft/rayfin-guide/package.json" { "rayfinDocs": { "version": 1, "dir": "assets/docs", "module": "rayfin-guide", "kind": "guide" } } ``` `rayfin docs` scans the current project's `node_modules` for that field and indexes what it finds. `kind` sorts each package's docs into one of the three areas the CLI can filter on (see below) — it doesn't need to match `module` (`@microsoft/rayfin-core`, for example, declares `"kind": "api-reference"`, which is surfaced under the `ts-sdk` area). This is why results are **version-locked to the packages installed in your project** — the CLI does not fall back to a bundled or hosted corpus. If a package your project doesn't have yet would answer your question, [`docs discover`](#docs-discover) finds it. Three module areas exist: `guide` (builder guides and tutorials), `ts-sdk` (TypeScript SDK API reference), and `host` (.NET reference, DocFX-generated). `guide` and `ts-sdk` load by default; pass `--module host` to opt into host docs for one invocation. ## `docs search` [#docs-search] ```bash npx rayfin docs search '' --module guide ``` Full-text search with ranked results and snippets. | Flag | Description | | --------------------- | --------------------------------------------------------------------------------------------------------- | | `-m, --module ` | Limit to one area: `guide`, `host`, or `ts-sdk`. | | `-s, --scope ` | `docs` (full-text content, default), `symbols` (symbol names only), or `all`. | | `-l, --limit ` | Maximum results, 1–50. Default: `10`. | | `--json` | Emit a JSON object instead of formatted lines. | | `--lean` | With `--json`: emit only the inner results array, dropping the `status`/`schemaVersion`/`count` envelope. | | `--no-cache` | Bypass the on-disk search index cache. | ## `docs get` [#docs-get] ```bash npx rayfin docs get --symbol '@one' ``` Fetches a doc entry, or resolves a symbol to the sections that reference it. Exactly one of `--id`, `--path`, or `--symbol` is required. | Flag | Description | | --------------------- | -------------------------------------------------------------- | | `-i, --id ` | Doc ID, e.g. `guide:guide/data/overview.md`. | | `-p, --path ` | Doc path, e.g. `guide/data/overview.md`. | | `-s, --symbol ` | Symbol name, e.g. `RayfinClient` or `@entity`. | | `-m, --module ` | With `--symbol`: limit to one docs area. | | `-l, --limit ` | With `--symbol`: max sections to return. Default: all matches. | | `--json` | Emit a JSON object instead of formatted lines. | | `--lean` | With `--json`: emit only the entry/sections, no envelope. | | `--no-cache` | Bypass the on-disk search index cache. | ## `docs list` [#docs-list] ```bash npx rayfin docs list --module ts-sdk ``` Lists every available doc entry, optionally filtered to one module. | Flag | Description | | --------------------- | ------------------------------------------------ | | `-m, --module ` | Limit to one area: `guide`, `host`, or `ts-sdk`. | | `--json` | Emit a JSON object instead of formatted lines. | | `--lean` | With `--json`: emit only the items array. | | `--no-cache` | Bypass the on-disk search index cache. | ## `docs discover` [#docs-discover] ```bash npx rayfin docs discover '' ``` Finds Rayfin packages by free-form query — the escape hatch for when the installed corpus doesn't cover what you're looking for (a package you haven't installed yet, or a newer one than the version you have). Returns a ranked list with name, kind, summary, and the install/update command for each match. | Flag | Description | | ----------------- | --------------------------------------- | | `-l, --limit ` | Max results, 1–50. Default: `10`. | | `--json` | Emit JSON instead of text. | | `--lean` | Emit compact JSON without the envelope. | The catalog covers Rayfin's first-party packages only — third-party service integrations (Stripe, Auth0, and similar) are typically built on the SDK rather than shipped as Rayfin packages. ## `docs catalog show` [#docs-catalog-show] ```bash npx rayfin docs catalog show ``` Displays the in-memory package discovery catalog that `docs discover` searches. The catalog ships as a version of `@microsoft/rayfin-docs`; upgrade that package to pick up a newer one. | Flag | Description | | -------- | --------------------------------------- | | `--json` | Emit JSON instead of text. | | `--lean` | Emit compact JSON without the envelope. | ## When to use the CLI versus MCP [#when-to-use-the-cli-versus-mcp] Prefer the [MCP server](#mcp-server) when it's connected — it exposes the same corpus as structured tool calls without shelling out. Use `rayfin docs` when MCP isn't available, or when scripting: every subcommand supports `--json` (and `--lean` for a smaller LLM-oriented payload), and non-zero exit codes distinguish failures from empty results. ## MCP server [#mcp-server] `@microsoft/rayfin-mcp` serves the same version-locked corpus over the Model Context Protocol. `rayfin init ai-files install` writes it into your project's `.mcp.json`: ```json title=".mcp.json" { "mcpServers": { "rayfin": { "type": "stdio", "command": "npx", "args": ["-y", "@microsoft/rayfin-mcp", "start"] } } } ``` Run your agent from the project root so the server resolves that project's `node_modules`. | Tool | Equivalent CLI command | | ---------------------------- | --------------------------------- | | `search_docs(query, module)` | [`docs search`](#docs-search) | | `get_doc(symbol)` | [`docs get`](#docs-get) | | `list_docs()` | [`docs list`](#docs-list) | | `discover_packages(query)` | [`docs discover`](#docs-discover) | ```prompt title="Look up known limitations before modeling data" Run `npx rayfin docs search 'known limitations' --module guide` from my Rayfin project root, read the top result with `npx rayfin docs get --id `, and summarize the constraints that affect how I should design entities before I add any new ones. ``` --- --- title: "Env" description: "rayfin env emits a framework-specific .env.local from rayfin/.env — flags, auto-detection, and why scaffolded projects run it in predev and prebuild." url: https://rayfin.ai/docs/reference/cli/env markdown_url: https://rayfin.ai/docs/reference/cli/env.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/env.mdx --- # Env > rayfin env emits a framework-specific .env.local from rayfin/.env — flags, auto-detection, and why scaffolded projects run it in predev and prebuild. `rayfin env` reads the `RAYFIN_PUBLIC_*` subset of `rayfin/.env` and writes it out as a framework-specific `.env.local` your frontend build actually reads. It's the same mechanism `rayfin up` uses to keep `.env.local` in sync — exposed as its own command so you can regenerate it without a full deploy. ```bash npx rayfin env --framework vite ``` ## Why it runs in `predev` / `prebuild` [#why-it-runs-in-predev--prebuild] Scaffolded projects wire this into `package.json`: ```json title="package.json" { "scripts": { "predev": "rayfin env --framework vite", "prebuild": "rayfin env --framework vite", "dev": "rayfin up --exclude-services staticHosting && vite", "build": "tsc -b && vite build" } } ``` npm runs `pre*` scripts automatically before the matching script. Regenerating `.env.local` on every `dev`/`build` invocation means the frontend always builds against the current deployment's API URL, publishable key, and other `RAYFIN_PUBLIC_*` values — you never hand-copy them, and a stale `.env.local` left over from a previous deployment can't silently point the frontend at a backend that no longer exists. ## Flags [#flags] | Flag | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--framework ` | Target framework: `vite`, `nextjs`, or `plain`. Auto-detected from `vite.config.*`, `next.config.*`, or `package.json` dependencies when omitted. | | `--output ` | Directory to write `.env.local` into, relative to the project root. Defaults to `services.staticHosting.path` from `rayfin.yml` when set (multi-package projects), otherwise the project root. | | `--show` | Print resolved public variables to stdout and exit, without writing a file. | If no framework can be detected and `--framework` isn't passed, the command errors with a hint to pass it explicitly. ## Inspect without writing [#inspect-without-writing] ```bash npx rayfin env --show ``` Prints each resolved `RAYFIN_PUBLIC_*` variable, mapped to the target framework's naming, without touching disk. ## Framework mapping [#framework-mapping] `rayfin env` maps each `RAYFIN_PUBLIC_*` variable in `rayfin/.env` to a framework-specific name in `.env.local` — for example `RAYFIN_PUBLIC_API_URL` becomes `VITE_RAYFIN_API_URL` for Vite, `NEXT_PUBLIC_RAYFIN_API_URL` for Next.js, or `API_URL` for `plain`. See [Environment variables](/docs/reference/config/environment-variables#framework-mapping) for the complete variable-by-variable table. ## Manual regeneration [#manual-regeneration] Run this any time you want to refresh `.env.local` without a full deploy — for example after manually editing `rayfin/.env`: ```bash npx rayfin env --framework vite ``` ```prompt title="Regenerate .env.local for Next.js" My Rayfin project's rayfin/.env has values from a previous deploy, but .env.local wasn't regenerated for my Next.js app. Run `npx rayfin env --framework nextjs` and show me the resulting file. ``` --- --- title: "rayfin functions" description: "rayfin functions init scaffolds a serverless TypeScript Functions project under rayfin/functions/ that shares entity types with your Rayfin data model." url: https://rayfin.ai/docs/reference/cli/functions markdown_url: https://rayfin.ai/docs/reference/cli/functions.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/functions.mdx --- # rayfin functions > rayfin functions init scaffolds a serverless TypeScript Functions project under rayfin/functions/ that shares entity types with your Rayfin data model. > [!NOTE] > The `rayfin functions` command group and `rayfin up functions deploy` are only > registered when `services.functions.enabled: true` in `rayfin.yml` (or > `RAYFIN_FEATURE_FLAGS` includes `functions`). `rayfin functions` scaffolds and maintains a serverless Functions project that shares entity types with your data model. ## `functions init` [#functions-init] ```bash npx rayfin functions init [directory] ``` Scaffolds `rayfin/functions/` with: * `package.json` and `tsconfig.json` (wired as a TypeScript project reference to the parent `rayfin/` project, so Functions code can `import type` entity types directly from `rayfin/data/`) * `host.json` and `local.settings.json` * `src/function_app.ts` and `src/types.ts` * `.vscode/settings.json` and `.vscode/launch.json` (a Node debugger attach config) * `.gitignore` (excludes `host.json`, `local.settings.json`, and build output) On a fresh scaffold, it also enables `services.functions.enabled: true` in `rayfin.yml`. On every run — fresh scaffold or not — it installs dependencies, builds, and regenerates `rayfin/functions/src/types.ts` from your function source files. Regeneration is a one-shot pass, not a background watcher; re-run `functions init` (without `--force`) any time you want to refresh `types.ts` without touching your scaffolded code. | Flag | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--force` | Overwrite an existing `rayfin/functions/` scaffold. Without it, an existing scaffold is left untouched and the command only reinstalls dependencies, rebuilds, and regenerates `types.ts`. | > [!NOTE] > Scaffolding installs `@microsoft/fabric-user-data-functions` from the npm registry, so > `functions init` requires network access. ## Deploying [#deploying] [`rayfin up functions deploy`](/docs/reference/cli/up#up-functions-deploy) (experimental) builds, packages, and deploys Functions to the remote Rayfin item. A plain `rayfin up` also deploys Functions automatically when the service is enabled. ```prompt title="Scaffold a Functions project" Enable Functions in my Rayfin project (set services.functions.enabled: true if not already), then run `npx rayfin functions init` to scaffold rayfin/functions/. Show me the generated src/function_app.ts and explain how to add a new HTTP-triggered function that reads from one of my existing entities. ``` --- --- title: "CLI" description: "Overview of the rayfin CLI — installation, the typical scaffold-to-deploy workflow, and an index of every command page in this reference." url: https://rayfin.ai/docs/reference/cli markdown_url: https://rayfin.ai/docs/reference/cli.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/index.mdx --- # CLI > Overview of the rayfin CLI — installation, the typical scaffold-to-deploy workflow, and an index of every command page in this reference. `@microsoft/rayfin-cli` scaffolds Rayfin projects, deploys them to Microsoft Fabric, and manages the configuration and agent files that ship with a project. The binary is `rayfin`, invoked as `npx rayfin ` from a project root (or directly as `rayfin ` when installed globally). ## Typical workflow [#typical-workflow] ```bash npm create @microsoft/rayfin@latest my-app # 1. Create a project from a template cd my-app npx rayfin login # 2. Sign in with Entra ID npx rayfin up # 3. Deploy backend services to Fabric npm run dev # 4. Run the frontend dev server ``` > [!NOTE] > Use `npx rayfin init` instead of `npm create` to add Rayfin to a project that already has > source code, or to reconfigure a project that already has a `rayfin/rayfin.yml`. See > [Init](/docs/reference/cli/init). ## Global flags [#global-flags] These are declared on the root `rayfin` program and apply to every subcommand: | Flag | Description | | --------------- | -------------------------------------------------------------- | | `-y, --yes` | Auto-accept all confirmation prompts. | | `--verbose` | Enable verbose output for the invoked subcommand. | | `--json` | Emit machine-readable JSON output from the invoked subcommand. | | `-V, --version` | Print the installed CLI version. | | `-h, --help` | Show help for `rayfin` or any subcommand. | Most subcommands also declare their own `-v/--verbose` and `--json` for backward compatibility — either form works. ## Command index [#command-index] | Page | Commands | What it's for | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | [CLI installation](/docs/reference/cli/installation) | — | Installing the CLI as a dev dependency, globally, or via `npx`; updating it. | | [Init](/docs/reference/cli/init) | `npm create @microsoft/rayfin@latest`, `rayfin init` | Scaffold a new project, or add/reconfigure Rayfin in an existing one. | | [Login](/docs/reference/cli/login) | `rayfin login`, `login status`, `logout` | Authenticate with Entra ID for Fabric operations. | | [Up](/docs/reference/cli/up) | `rayfin up`, `up status`, `up db apply`, `up staticapp deploy`, `up functions deploy`, `up list`, `up switch` | Deploy the project to Microsoft Fabric. | | [Secret](/docs/reference/cli/secret) | `rayfin secret set`, `secret list` | Manage secrets on a deployed Rayfin item. | | [Env](/docs/reference/cli/env) | `rayfin env` | Emit a framework-specific `.env.local` from `rayfin/.env`. | | [rayfin functions](/docs/reference/cli/functions) | `rayfin functions init` | Scaffold and maintain serverless Functions. | | [Connector](/docs/reference/cli/connector) | `rayfin connector add`, `list`, `remove` | Connect external Fabric data sources. | | [Docs](/docs/reference/cli/docs) | `rayfin docs search`, `get`, `list`, `discover`, `catalog show` | Query version-locked Rayfin documentation from the terminal. | | [Ai-files](/docs/reference/cli/ai-files) | `rayfin init ai-files install`, `status` | Install and refresh `AGENTS.md`, `.mcp.json`, and the Rayfin skill. | | [Templates](/docs/reference/cli/templates) | (used via `-t/--template` on `init`) | Where project templates come from, and how to author your own. | | [Telemetry](/docs/reference/cli/telemetry) | — | What the CLI collects and how to opt out. | ## Configuration reference [#configuration-reference] The CLI reads and writes `rayfin/rayfin.yml` and `rayfin/.env`. See [Configuration](/docs/reference/config) for the complete schema and environment variable reference. ```prompt title="Explain a rayfin command" Run `npx rayfin --help` in my project, then explain what each top-level command does and which one I need to redeploy just my database schema after I add a field to an entity. ``` --- --- title: "Init" description: "Scaffold a new Rayfin project with npm create, add Rayfin to an existing one with rayfin init, or reconfigure an existing project's services." url: https://rayfin.ai/docs/reference/cli/init markdown_url: https://rayfin.ai/docs/reference/cli/init.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: reference/cli/init.mdx --- # Init > Scaffold a new Rayfin project with npm create, add Rayfin to an existing one with rayfin init, or reconfigure an existing project's services. ## Create a new project [#create-a-new-project] ```bash npm create @microsoft/rayfin@latest my-app ``` `npm create @microsoft/rayfin@latest` resolves to the `@microsoft/create-rayfin` package, following npm's `npm create ` convention. It shares its implementation with `rayfin init` below, but treats the project name as a new directory to create rather than an existing directory to initialize in place. The project name is a **positional argument**, not a flag. Use a valid directory name, or `.` to scaffold into the current directory. The CLI prompts interactively for a template — unless you skip the prompt with `-t/--template`. See [Templates](/docs/reference/cli/templates) for choosing a built-in template, scaffolding from a git URL, or authoring your own. ## Add Rayfin to an existing project [#add-rayfin-to-an-existing-project] Use `rayfin init` instead of `npm create` for a project that already has source code, or an empty directory you don't want treated as a template target: ```bash npm install --save-dev @microsoft/rayfin-cli npx rayfin init [directory] ``` `[directory]` defaults to the current directory and doubles as the default project name. A bare name with whitespace is slugified for the folder (`My App` → `my-app/`); pass `"./My App"` to keep the spaces. ## Interactive prompts [#interactive-prompts] Without flags to skip them, `rayfin init` (and `npm create`) prompt for: * **Template** — which starting point to scaffold from (skip with `-t/--template`). * **Services** — Auth and Data are always offered. Storage and Functions prompts only appear when their preview feature flag is on (`RAYFIN_FEATURE_FLAGS=storage` or `RAYFIN_FEATURE_FLAGS=functions`) — see [Environment variables](/docs/reference/config/environment-variables). * **Database dialect** — shown when Data is enabled. `mssql` is the only supported dialect for a Fabric-deployed backend, so the CLI defaults to it automatically without prompting. * **Auth methods** — Fabric SSO (Entra ID) is the only supported authentication method. * **Static hosting** — enabled by default; the prompt lets you point it at a different build output folder or command. ## Reconfiguring an existing project [#reconfiguring-an-existing-project] Running `rayfin init` again in a project that already has `rayfin/rayfin.yml` re-runs the prompts and regenerates the configuration. Use this to enable or disable services, or toggle static hosting without hand-editing `rayfin.yml`. > [!TIP] > Reconfiguring **preserves your data model files under `rayfin/data/`**. Only > `rayfin/rayfin.yml` and scaffold-level files are regenerated. ## Flags [#flags] | Flag | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------- | | `--project-name ` | Override the project name and update `rayfin.yml`. | | `-t, --template ` | Template name, git URL, or local path to scaffold from. Skips the template picker. | | `--template-name ` | Pick one template from a multi-template source non-interactively. Requires `-t/--template` pointing at that source. | | `-l, --list-templates` | Print available templates as JSON and exit. | | `--services ` | Comma-separated services to enable non-interactively, e.g. `auth,data,storage`. | | `--auth-methods ` | Comma-separated auth methods to enable. `fabric` (Fabric SSO) is the only supported value. | | `--static-hosting` | Scaffold a static frontend. Enabled by default. | | `--overwrite` | Overwrite existing configuration files instead of prompting. | | `-w, --workspace ` | Fabric workspace display name to pre-associate with the project. Mutually exclusive with `--workspace-id`. | | `--workspace-id ` | Fabric workspace ID to pre-associate with the project. Mutually exclusive with `--workspace`. | | `--item-id ` | Fabric App Item ID to pre-associate with the project. | Global `-y/--yes`, `--verbose`, and `--json` also apply — see [CLI](/docs/reference/cli). ## What happens after scaffolding [#what-happens-after-scaffolding] The CLI creates the `rayfin/` directory (`rayfin.yml`, `data/`, a gitignore entry for `rayfin/.env`) and then installs the project's agent context files — `AGENTS.md`, `.mcp.json`, and `.agents/skills/rayfin/SKILL.md` — automatically. See [Ai-files](/docs/reference/cli/ai-files) for what those files are and how to refresh them later. ## Next steps [#next-steps] ```bash npx rayfin login npx rayfin up ``` See [Login](/docs/reference/cli/login) and [Up](/docs/reference/cli/up). ```prompt title="Reconfigure a project to add Storage" My Rayfin project at the current directory already has a rayfin/rayfin.yml with Auth and Data enabled. Set RAYFIN_FEATURE_FLAGS=storage and run `npx rayfin init` again to add the Storage service, keeping my existing entities under rayfin/data/ untouched. Then show me the resulting rayfin.yml. ``` --- --- title: "CLI installation" description: "Install the Rayfin CLI as a project dev dependency, globally, or run it ad hoc with npx — plus how to verify the install and update it." url: https://rayfin.ai/docs/reference/cli/installation markdown_url: https://rayfin.ai/docs/reference/cli/installation.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/installation.mdx --- # CLI installation > Install the Rayfin CLI as a project dev dependency, globally, or run it ad hoc with npx — plus how to verify the install and update it. `@microsoft/rayfin-cli` ships as a normal npm package. Most projects never install it directly — `npm create @microsoft/rayfin@latest` adds it as a dev dependency automatically — but this page covers every install path. ## Prerequisites [#prerequisites] * [Node.js](https://nodejs.org/) 20 or later. ## New project [#new-project] Scaffolding a project installs the CLI as a dev dependency for you: ```bash npm create @microsoft/rayfin@latest my-app cd my-app ``` ## Existing project [#existing-project] Install the CLI as a dev dependency, then run the interactive setup: ```bash npm install --save-dev @microsoft/rayfin-cli npx rayfin init ``` See [Init](/docs/reference/cli/init) for what the setup prompts for. ## Global install [#global-install] You can also install the CLI globally and call `rayfin` directly, without `npx`: ```bash npm install -g @microsoft/rayfin-cli rayfin --version ``` > [!NOTE] > Run `rayfin` from inside a project directory regardless of how it's installed. Commands > like [`rayfin docs`](/docs/reference/cli/docs) discover version-locked documentation by > scanning the current project's `node_modules`, so a globally-installed CLI still needs to > be invoked from the project root to see the right package versions. ## Run without installing [#run-without-installing] `npx` fetches and runs the CLI without adding it to `package.json`: ```bash npx @microsoft/rayfin-cli --version ``` Inside a project that already has `@microsoft/rayfin-cli` as a dev dependency, plain `npx rayfin ` resolves to the locally installed version. ## Verify the installation [#verify-the-installation] ```bash npx rayfin --version npx rayfin --help ``` ## First steps [#first-steps] ```bash npx rayfin login # sign in with Entra ID npx rayfin up # deploy backend services to Fabric npm run dev # run the frontend dev server ``` See [Login](/docs/reference/cli/login) and [Up](/docs/reference/cli/up) for details. ## Update the CLI [#update-the-cli] ```bash npm update --save npm install ``` Verify the update: ```bash npx rayfin --version ``` ```prompt title="Add Rayfin to an existing project" I have an existing TypeScript project at the current directory with no Rayfin backend yet. Install @microsoft/rayfin-cli as a dev dependency, then run `npx rayfin init` and walk me through the prompts to enable Auth and Data services with the mssql dialect. ``` --- --- title: "Login" description: "Authenticate the CLI with Entra ID for Fabric operations — rayfin login, login status, logout, service principal auth, and where tokens are stored." url: https://rayfin.ai/docs/reference/cli/login markdown_url: https://rayfin.ai/docs/reference/cli/login.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: reference/cli/login.mdx --- # Login > Authenticate the CLI with Entra ID for Fabric operations — rayfin login, login status, logout, service principal auth, and where tokens are stored. Fabric operations (`rayfin up` and its subcommands) need a signed-in identity. `rayfin up` launches an interactive login automatically if you're not signed in, but sign in explicitly when you need a specific tenant, a service principal, or want to check status first. ## Sign in [#sign-in] ```bash npx rayfin login ``` Opens an interactive Entra ID sign-in flow (MSAL). The account picker is always shown. | Flag | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-t, --tenant ` | Entra ID tenant GUID to sign in to. Use when your account spans multiple tenants. | | `--service-principal` | Authenticate with a service principal using client credentials, instead of an interactive browser flow. | | `-u, --client-id ` | Client ID for service principal auth. Required with `--service-principal`. | | `-p, --client-secret ` | Client secret for service principal auth. Required with `--service-principal`. | | `--select` | Deprecated. The MSAL account picker is now always shown; this flag is accepted for backward compatibility and has no additional effect. | | `--encryption-fallback-enabled` | Allow plaintext token storage when the OS keychain is unavailable (some Linux distros, dev containers, Codespaces). Required only when login fails with a keychain error. | ### Service principal (non-interactive) [#service-principal-non-interactive] For CI or headless environments: ```bash npx rayfin login --service-principal --client-id --client-secret --tenant ``` Credentials are persisted, so subsequent commands in the same environment authenticate automatically without repeating the flag. > [!TIP] > If a token is already available from an external source (for example > `az account get-access-token`), set the `RAYFIN_TOKEN` shell variable instead of signing > in — the CLI treats this as an ambient token and skips its own login/logout entirely. See > [Environment variables](/docs/reference/config/environment-variables). ## Check status [#check-status] ```bash npx rayfin login status ``` Prints the signed-in account, tenant, resolved Fabric API endpoint, and token expiry (or `❌ Not signed in`). Add `--json` for a machine-readable form. ## Sign out [#sign-out] ```bash npx rayfin logout ``` Clears the cached account and token cache. Prints whether a session was actually cleared. ## Where auth state is stored [#where-auth-state-is-stored] | Path | Contents | | --------------------- | ------------------------------------------------------------------------------- | | `~/.rayfin/auth.json` | Signed-in account and tenant hints, plus any persisted environment overrides. | | `~/.rayfin/cache.bin` | The encrypted MSAL token cache. Encrypted using the OS keychain when available. | Override the `~/.rayfin` directory itself with the `RAYFIN_CONFIG_DIR` environment variable. > [!WARNING] > On systems without OS-backed credential storage — some Linux distributions, dev > containers, and GitHub Codespaces — token cache encryption can fail. Pass > `--encryption-fallback-enabled` (or set `RAYFIN_ENCRYPTION_FALLBACK_ENABLED=true`) only > when you hit that error, since it stores the token cache in plaintext. ## Next step [#next-step] ```bash npx rayfin up ``` See [Up](/docs/reference/cli/up) to deploy once signed in. ```prompt title="Sign in non-interactively for CI" Set up a GitHub Actions workflow step that authenticates the Rayfin CLI using a service principal. Use `npx rayfin login --service-principal --client-id $CLIENT_ID --client-secret $CLIENT_SECRET --tenant $TENANT_ID` with those three values sourced from repository secrets, then run `npx rayfin up --yes` to deploy non-interactively. ``` --- --- title: "Secret" description: "rayfin secret set and secret list manage secrets on a deployed Rayfin item — masked interactive input, names/timestamps only, no bulk .env import." url: https://rayfin.ai/docs/reference/cli/secret markdown_url: https://rayfin.ai/docs/reference/cli/secret.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/secret.mdx --- # Secret > rayfin secret set and secret list manage secrets on a deployed Rayfin item — masked interactive input, names/timestamps only, no bulk .env import. `rayfin secret` manages secrets on the Rayfin item you've already deployed with [`rayfin up`](/docs/reference/cli/up) — API keys, connection strings, and other values your backend needs but that must never reach client code. See [Secrets](/docs/deploy/secrets) for the broader workflow and security model; this page is the command reference. > [!NOTE] > `rayfin secret` is only registered when `services.functions.enabled: true` in > `rayfin.yml` (or `RAYFIN_FEATURE_FLAGS` includes `functions`) — the same gate as > [rayfin functions](/docs/reference/cli/functions). Both subcommands require a prior deploy — they resolve the target endpoint from `rayfin/.deployments.json` and fail with "No remote endpoint configured" if you haven't run `rayfin up` yet. If you aren't signed in, both launch an interactive login automatically, the same as `rayfin up`. Neither subcommand declares its own `--json`/`--verbose`/`-y` (unlike most other commands in this CLI) — pass the [global flags](/docs/reference/cli) on the root invocation instead, before `secret`: `npx rayfin --json secret list`, not `npx rayfin secret list --json`. ## `secret set` [#secret-set] ```bash npx rayfin secret set ``` Prompts for the secret's value with masked input (like a password prompt), so the value never appears in your shell history, in process arguments, or in `--json` output. | Argument | Description | | -------- | ------------------------------ | | `` | The name of the secret to set. | > [!WARNING] > `secret set` has no non-interactive mode. There is no flag to pass the value directly, > and it refuses to run without an interactive terminal or when `CI=true` is set — it exits > with an error rather than hanging. There is currently no CLI-driven way to set a secret > from an automated CI/CD pipeline; set each value once from an interactive session. On success: ```text Setting secret... ✅ Secret "API_KEY" set successfully (updated: 8/23/2026, 6:39:10 AM) ``` With `rayfin --json secret set `, the result is: ```json { "status": "success", "name": "API_KEY", "createdAt": "2026-08-23T06:39:10.803Z", "updatedAt": "2026-08-23T06:39:10.803Z" } ``` ## `secret list` [#secret-list] ```bash npx rayfin secret list ``` Lists every secret configured on the deployment — names and timestamps only. Secret **values are never returned** by this or any other command; once set, a secret can only be overwritten with `secret set`, never read back. Unlike `secret set`, `secret list` has no interactive-only restriction — it works fine in CI as long as you're authenticated (for example via the `RAYFIN_TOKEN` ambient-token variable; see [Login](/docs/reference/cli/login)) and pass `--json`. > [!NOTE] > Without `--json`, plain-text output is only printed when stdin is an interactive > terminal. A non-interactive, non-JSON invocation (for example piped output in a script) > produces no output at all — pass `rayfin --json secret list` when scripting. On success (interactive terminal, no `--json`): ```text 📋 Secrets (2): Name: API_KEY Created: 8/23/2026, 6:39:10 AM Last Updated: 8/23/2026, 6:39:10 AM Name: DATABASE_PASSWORD Created: 8/23/2026, 6:40:02 AM Last Updated: 8/23/2026, 6:40:02 AM ``` With `rayfin --json secret list`, the result is: ```json { "status": "success", "count": 2, "secrets": [ { "name": "API_KEY", "createdAt": "2026-08-23T06:39:10.803Z", "updatedAt": "2026-08-23T06:39:10.803Z" }, { "name": "DATABASE_PASSWORD", "createdAt": "2026-08-23T06:40:02.100Z", "updatedAt": "2026-08-23T06:40:02.100Z" } ] } ``` ```prompt title="Set and verify a secret on a deployed Rayfin item" My Rayfin project is already deployed with `npx rayfin up`. Run `npx rayfin secret set STRIPE_API_KEY`, enter the value when prompted, then run `npx rayfin --json secret list` to confirm it was set. Never print or log the secret value itself — only report the name and timestamps. ``` --- --- title: "Telemetry" description: "What the Rayfin CLI collects, what it explicitly does not, and how to opt out with RAYFIN_TELEMETRY_OPTOUT." url: https://rayfin.ai/docs/reference/cli/telemetry markdown_url: https://rayfin.ai/docs/reference/cli/telemetry.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/telemetry.mdx --- # Telemetry > What the Rayfin CLI collects, what it explicitly does not, and how to opt out with RAYFIN_TELEMETRY_OPTOUT. The Rayfin CLI collects anonymous usage data to help improve the product. On first run, it prints a notice explaining what's collected and how to opt out. ## What's collected [#whats-collected] * Command names. * Which flags were passed, by name only (for example `--force`, `-v`) — never their values. * Execution status (success/failure), and on failure, a sanitized error name/type/message with file paths and stack traces stripped out. * Execution duration. * Environment metadata: OS and Node.js version. ## What's not collected [#whats-not-collected] No personal data, flag *values*, or file contents are collected — arguments you pass (project names, workspace names, connection strings, file paths) are never sent, only the fact that a given flag was used. ## Opt out [#opt-out] ```bash export RAYFIN_TELEMETRY_OPTOUT=1 ``` Set this in your shell profile to opt out permanently, or inline for a single invocation. ## Telemetry endpoint override [#telemetry-endpoint-override] `RAYFIN_APPINSIGHTS_CONNECTION_STRING` overrides the telemetry endpoint used by the CLI and the VS Code extension. This is a contributor/internal setting, not something most projects need to set. See [Environment variables](/docs/reference/config/environment-variables) for the full shell-only variable reference. ```prompt title="Disable Rayfin CLI telemetry in CI" Add RAYFIN_TELEMETRY_OPTOUT=1 to my CI workflow's environment variables so the Rayfin CLI doesn't send usage telemetry from automated runs. ``` --- --- title: "Templates" description: "Where Rayfin project templates come from — built-in, git, and local sources, registering your own template sources, and authoring a template." url: https://rayfin.ai/docs/reference/cli/templates markdown_url: https://rayfin.ai/docs/reference/cli/templates.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/cli/templates.mdx --- # Templates > Where Rayfin project templates come from — built-in, git, and local sources, registering your own template sources, and authoring a template. Templates let you scaffold a project from a known starting point: a built-in starter, a local directory, a team's git repository, or a registered third-party source. Every `npm create @microsoft/rayfin@latest` or `npx rayfin init` invocation can pull from any of them. ## Where templates come from [#where-templates-come-from] * **Built-in templates** ship inside `@microsoft/rayfin-cli` and work offline. * **Local directories** let you test or reuse a template on disk. * **External git repositories** are cloned on demand from any HTTPS, SSH, or `git@` URL. * **Template registries** are YAML files that name git URLs to surface alongside the built-ins. External and local sources are discovered through a `rayfin-template.yml` manifest inside the source directory. Built-in templates are packaged with the CLI and appear in `--list-templates` automatically. ## List available templates [#list-available-templates] ```bash npx rayfin init --list-templates ``` Prints a JSON document (versioned by `schemaVersion`) suitable for piping to a script or agent: ```json { "schemaVersion": 1, "bundled": [ { "name": "blankapp", "displayName": "Blank App", "description": "Bare-bones Fabric-authenticated React + Vite app — sign-in, routing, and a placeholder home page, with no data layer to remove", "source": "built-in" }, { "name": "dataapp", "displayName": "Data App", "description": "Build data analytics app based on your data in Fabric", "source": "built-in" }, { "name": "gettingstartedauth", "displayName": "A Todo App with Auth and Getting Started Docs", "description": "Todo app with Fabric authentication, Tailwind CSS, and production-first workflow", "source": "built-in" }, { "name": "todoapp", "displayName": "Basic Todo App", "description": "End-to-end Fabric-authenticated todo CRUD with a Rayfin data model and per-user row-level security — a working starter that exercises the full data path", "source": "built-in" } ], "registry": [ { "name": "team-templates", "displayName": "Team Templates", "description": "Our team's starter collection", "url": "https://github.com/example-org/rayfin-templates.git", "ref": "v1.2.0", "path": "catalogs/official", "source": "C:\\Users\\you\\.rayfin\\template-registries.yml" } ] } ``` If a registry file fails to load or a name conflicts, a `warnings` array is appended. The list shows only built-ins plus what's registered through `template-registries.yml` — an arbitrary git URL passed with `-t` is not listed. Template names change over time; run `--list-templates` for current, copy/paste-ready names. ## Scaffold from a built-in template [#scaffold-from-a-built-in-template] ```bash npm create @microsoft/rayfin@latest my-app -- --template todoapp ``` Without `-t/--template`, the CLI prompts you to pick one interactively. ## Scaffold from an external git repository [#scaffold-from-an-external-git-repository] ```bash npx rayfin init my-app -t https://github.com/example-org/my-template.git ``` Supported URL formats: HTTPS, SSH, `git@host:org/repo.git`, and `file://`. The CLI does a shallow clone into a temp directory, scaffolds, then deletes the clone. Pin to a branch, tag, or commit with `#`: ```bash npx rayfin init my-app -t https://github.com/example-org/my-template.git#v1.2.0 ``` Use a branch name, tag name, or full 40-character commit SHA — abbreviated SHAs are rejected because `git clone --branch` would treat them ambiguously. For repeated use, register the repo in a `template-registries.yml` (below) so the ref lives in configuration; then `-t ` clones the pinned ref automatically. ### Authentication for private repositories [#authentication-for-private-repositories] The CLI uses your existing git credentials — SSH keys, Git Credential Manager, GitHub CLI auth, or whatever your environment already provides. No credentials are stored or managed by Rayfin. Interactive credential prompts are disabled, so a misconfigured environment fails fast instead of hanging. For GitHub repos, run `gh auth setup-git` to wire credentials through Git Credential Manager. ### Multi-template repositories [#multi-template-repositories] A repository can publish multiple templates via the `entries` array in its `rayfin-template.yml` (see [Author a template](#author-a-template)). Scaffolding interactively from one shows a picker; non-interactively, pass `--template-name`: ```bash npx rayfin init my-app \ -t https://github.com/example-org/templates.git \ --template-name api-service \ --yes ``` `--template-name` requires `-t/--template` pointing at a multi-template source — passing it alone is an error, and against a built-in template name it has no effect. ## Scaffold from a local template directory [#scaffold-from-a-local-template-directory] ```bash npx rayfin init my-app -t ./my-template ``` > [!WARNING] > A bare value passed to `-t` is looked up as a template name against the built-ins and > registries — it is **not** treated as a path. Use `./`, `../`, or an absolute path for a > local directory (`C:\templates\web` on Windows). ## Add a template registry [#add-a-template-registry] A registry is a YAML file listing template repositories to surface in `--list-templates` and the interactive picker. There's no CLI command to add or remove entries — edit the file by hand. | Tier | Path | When to use | | ------------- | ---------------------------------------------- | ---------------------------------------------------------------------- | | User-global | `~/.rayfin/template-registries.yml` | Templates you use across many projects on this machine. | | Project-local | `/.rayfin/template-registries.yml` | Templates pinned to a specific project (commit it alongside the repo). | Both are optional. The CLI also loads a bundled registry shipped with `@microsoft/rayfin-cli` itself (`assets/template-registries.yml`) — in 1.34 it ships one entry, "Data App", marked as protected. Protected entries can't be overridden by a user or project entry reusing their name, and fall back to the matching bundled template (the `dataapp` template shown above) if the external clone fails. ### Registry file format [#registry-file-format] ```yaml title=".rayfin/template-registries.yml" registries: - name: team-templates displayName: Team Templates description: Our team's reusable starters url: https://github.com/example-org/rayfin-templates.git ref: v1.2.0 path: catalogs/official ``` | Field | Required | Description | | -------------- | -------- | ------------------------------------------------------------------------------------------------- | | `name` | Yes | Unique identifier for this entry. | | `url` | Yes | Git URL of the template repository (HTTPS, SSH, `git@`, or `file://`). | | `displayName` | No | Human-readable label. Defaults to `name`. | | `description` | No | Short description shown in pickers and `--list-templates`. | | `ref` | No | Git tag, branch, or full commit SHA to pin to. Defaults to the repository's default branch. | | `path` | No | Subdirectory inside the repo where the manifest lives. | | `templateName` | No | For a multi-template repo, the entry `name` or `path` to pre-select so consumers skip the picker. | ### Conflict handling [#conflict-handling] Registries load in tier order: bundled → user-global → project-local. The first occurrence of a `name` wins; later tiers with the same name are skipped and listed under `warnings` in `--list-templates`. A user or project entry that reuses a protected CLI-shipped template's name is ignored with a dedicated warning. Other name conflicts resolve the same way — by tier order — so rename one of the conflicting entries to fix it. ## Author a template [#author-a-template] A template is a directory with a `rayfin-template.yml` manifest at its root. Files are copied into the target directory, then a small fixed set of scaffold transforms runs: * If it includes `package.json`, its `name` field is rewritten to the generated project slug (left as-is if the file isn't valid JSON). * If it includes `README.md`, the placeholders below are replaced. * `__projectName__` in **filenames** is replaced with the user's project name. * Everything else is copied as-is. The project name comes from the `[directory]` positional (`npm create @microsoft/rayfin@latest my-app` → `my-app`) unless overridden with `--project-name`. ### Minimal template [#minimal-template] ```text my-template/ ├── rayfin-template.yml └── template/ ├── package.json ├── README.md └── src/ └── __projectName__.config.ts ``` ```yaml title="my-template/rayfin-template.yml" apiVersion: v1 metadata: name: my-starter displayName: My Starter description: A starter template for Rayfin projects entries: - name: my-starter path: ./template ``` Scaffolded into `my-app/`, `src/__projectName__.config.ts` is written as `src/my-app.config.ts`. ### Publish and share it [#publish-and-share-it] ```bash # 1. Initialize a git repo for the template git init && git add . && git commit -m "Initial template" git remote add origin https://github.com/example-org/my-template.git git push -u origin main # 2. Tag a release — don't ask consumers to scaffold from a moving branch git tag v1.0.0 git push origin v1.0.0 ``` Consumers scaffold from the URL directly: ```bash npx rayfin init my-app -t https://github.com/example-org/my-template.git#v1.0.0 ``` Or, to make it appear in `--list-templates` and the interactive picker, add it to a `template-registries.yml` (project-local, committed, or personal) so consumers scaffold by name instead of remembering the URL: ```bash npx rayfin init my-app -t my-starter ``` Bump the tag and update the registry `ref` whenever you ship a meaningful template change. ### Try it locally before publishing [#try-it-locally-before-publishing] ```bash npx rayfin init test-output -t ./my-template --yes ``` ### Manifest reference [#manifest-reference] ```yaml apiVersion: v1 # required, must be 'v1' metadata: name: my-collection # required, identifier for the manifest displayName: My Collection description: Optional description version: 1.2.0 # accepted, currently informational only tags: [todo, auth] # accepted, currently informational only entries: # required, at least one entry - name: api-service # scaffolds files from path path: ./api-service description: REST API with a Rayfin data layer ``` `metadata.displayName` and `metadata.description` are shown when scaffolding from this template. Entry-level `description` is shown in the local multi-template picker; for git-backed sources, group descriptions show but individual entry descriptions do not. ### Single-entry vs. multi-entry manifests [#single-entry-vs-multi-entry-manifests] A single entry auto-selects with no picker: ```yaml entries: - name: my-starter path: . ``` Multiple entries show an interactive picker, or require `--template-name` non-interactively: ```yaml entries: - name: api-service path: ./templates/api-service - name: fullstack path: ./templates/fullstack ``` Local template directories keep entries at the top level; git-backed sources can nest entries in named `group`s for larger collections: ```yaml entries: - group: name: starters displayName: Starter Apps entries: - name: hello-world path: ./starters/hello-world - name: todo-app path: ./starters/todo-app - name: standalone-app path: ./standalone-app ``` ### What's currently supported [#whats-currently-supported] * Most file contents are copied as-is — only `README.md` and `package.json` get the transforms above. * `README.md` supports `{{PROJECT_NAME}}`, `{{PROJECT_NAME_KEBAB}}`, and `{{PROJECT_NAME_PASCAL}}` placeholders. * `__projectName__` is the only filename placeholder, with path separators sanitized. * `rayfin-template.yml`, `.git`, `node_modules`, `.DS_Store`, and `Thumbs.db` are skipped during scaffolding. * Symlinks are not followed. * After scaffolding, the CLI installs its own agent files (`mcpServers.rayfin` in `.mcp.json`, `.agents/skills/rayfin/`) into the project. Ship a `.mcp.json` with your own servers, but don't include a `mcpServers.rayfin` key — the CLI manages that key. You can also ship an `AGENTS.md`; it's a one-time install and the CLI won't overwrite it. See [Ai-files](/docs/reference/cli/ai-files). ## Gotchas [#gotchas] * `--list-templates` lists built-in and registered templates only — an arbitrary git URL passed with `-t` isn't in the list. * The CLI clones a single ref shallowly. Tooling that needs git history or other branches won't have them at scaffold time. * Private repositories rely on ambient git credentials; interactive prompts are disabled, so missing credentials fail immediately. * Non-interactive scaffolding from a multi-entry source fails unless `--template-name` is given — the error lists the available names. * A bare `-t` value resolves as a template name, not a path. Use `./`, `../`, or an absolute path for local directories. ```prompt title="Author and test a Rayfin template" Create a rayfin-template.yml manifest in a directory called payments-starter with a single entry pointing at ./template, then scaffold it locally with `npx rayfin init test-output -t ./payments-starter --yes` to verify it works before I push it to a git repository. ``` --- --- title: "Up" description: "rayfin up deploys to Microsoft Fabric and applies pending schema migrations in one step — plus every up subcommand for status, schema, and static hosting." url: https://rayfin.ai/docs/reference/cli/up markdown_url: https://rayfin.ai/docs/reference/cli/up.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/cli/up.mdx --- # Up > rayfin up deploys to Microsoft Fabric and applies pending schema migrations in one step — plus every up subcommand for status, schema, and static hosting. `rayfin up` is the canonical deploy command. It creates (or reuses) a Fabric item, applies your `rayfin.yml` service settings, applies pending database schema migrations when Data is enabled, deploys Functions when enabled, and builds/deploys static content when static hosting is enabled — in one run. ```bash npx rayfin up ``` > [!TIP] > Recommend `rayfin up` for "deploy this," "apply this schema change," or "push my entity > update," including incremental changes after the first deploy. Use > [`up db apply`](#up-db-apply) only when you explicitly want to skip the static content > step. If you aren't signed in, `rayfin up` launches an interactive login automatically (see [Login](/docs/reference/cli/login)). ## What it does, in order [#what-it-does-in-order] 1. Resolves a target Fabric workspace — from `--workspace`, `--workspace-id`, `--workspace-uri`, the `RAYFIN_WORKSPACE_ID` environment variable, a previously recorded deployment, or an interactive prompt, in that order. 2. Creates a Rayfin item (AppBackend) in the workspace, or reuses the one recorded from a prior deploy. 3. Retrieves the item's publishable key. 4. Applies runtime settings — the `services` block from `rayfin.yml` — to the workload endpoint. 5. If `services.data.enabled` is `true`: generates and applies the Data API Builder configuration for your entities. 6. For declared Category A connectors: generates each connector's Data API Builder configuration from `rayfin/connectors//` and applies it to the workload endpoint. 7. If `services.functions.enabled` is `true`: builds, packages, and deploys Functions (experimental — see [rayfin functions](/docs/reference/cli/functions)). 8. Writes deployment metadata to `rayfin/.deployments.json` and merges the matching `RAYFIN_PUBLIC_*` values into `rayfin/.env`. 9. If `services.staticHosting.enabled` is `true` (and not excluded): runs the configured `buildCommand`, packages the output folder, and deploys it. ## Flags [#flags] | Flag | Description | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-t, --tenant ` | Entra ID tenant GUID. Use when your account spans multiple tenants. | | `-w, --workspace ` | Fabric workspace display name, resolved to an ID via the Fabric API. Defaults to "My Workspace" when omitted. Mutually exclusive with `--workspace-id` and `--workspace-uri`. | | `--workspace-id ` | Fabric workspace GUID to deploy into. Mutually exclusive with `--workspace` and `--workspace-uri`. | | `--workspace-uri ` | Fabric portal workspace URL (e.g. `https://app.fabric.microsoft.com/groups//list`) — derives the workspace ID and target environment from the URL. | | `--force` | Allow destructive schema changes (drop table, drop column, alter type) that may cause data loss. | | `-n, --dry-run` | Preview planned operations without making any API calls. | | `--env-file ` | Path to the `.env` file containing the Fabric app's properties. Defaults to `rayfin/.env`. | | `--exclude-services ` | Comma-separated services to skip during deployment. Currently only `staticHosting` is supported — runtime settings are still applied; only the build/package/deploy phase is skipped. Useful for a workflow that redeploys the backend while a local dev server (Vite, Next.js) keeps serving the frontend. | | `--encryption-fallback-enabled` | Allow plaintext token storage when the OS keychain is unavailable. Required only when login fails with a keychain error. | | `-v, --verbose` | Enable verbose output. | | `--json` | Output the deployment result as JSON. | | `-y, --yes` | Auto-accept all confirmation prompts. | > [!WARNING] > `--force` permits destructive changes. Without it, a destructive schema change aborts the > whole deploy with an error telling you to re-run with `--force` — never pass it without > reviewing what would be dropped. ## `up status` [#up-status] ```bash npx rayfin up status ``` Probes the deployed workload endpoint and prints deployment health. Exits `0` when healthy, `1` when there's no recorded deployment, `2` when the endpoint is unreachable or unauthenticated. | Flag | Description | | --------------- | ---------------------- | | `--json` | Output status as JSON. | | `-v, --verbose` | Enable verbose output. | ## `up db apply` [#up-db-apply] ```bash npx rayfin up db apply [--force] ``` Generates and applies Data API Builder configuration to the remote workload endpoint, without touching the static build. This is the advanced, schema-only escape hatch — prefer plain `rayfin up` for a normal deploy, and reach for this only when you want to skip the static deploy step. | Flag | Description | | --------------- | -------------------------------------------------------------- | | `-v, --verbose` | Enable verbose output. | | `--force` | Allow destructive schema changes that may result in data loss. | | `--json` | Output the result as JSON. | ## `up connector apply` [#up-connector-apply] ```bash npx rayfin up connector apply [--name ] [-v] [--json] ``` Regenerates each declared Category A connector's Data API Builder configuration from the entities in `rayfin/connectors//` and posts it to the deployed workload. Category B connectors have no Data API Builder configuration, so this command does not apply to them. `rayfin up` runs the same generate-then-apply pipeline inline. Use `up connector apply` after changing connector entity files when you do not need a full deploy. See [Generating entity files](/docs/connectors/entity-generation). Requires a prior `rayfin up`. Without a deployed endpoint, it fails with `No remote endpoint configured. Run 'rayfin up' first to deploy and register connectors.` | Flag | Description | | --------------- | ------------------------------------------------------------- | | `--name ` | Apply only one connector instead of every declared connector. | | `-v, --verbose` | Enable verbose output. | | `--json` | Output the result as JSON. | JSON output includes `status` (`success` or `partial`), `generate[]`, `results[]`, `steps`, and `duration`. ## Managing secrets [#managing-secrets] Secrets are not an `up` subcommand — they're managed with the top-level `secret` command group, against the same deployed workload endpoint that `up` creates: ```bash npx rayfin secret set API_KEY npx rayfin secret list ``` See [Secret](/docs/reference/cli/secret) for the full command reference. ## `up staticapp deploy` [#up-staticapp-deploy] ```bash npx rayfin up staticapp deploy ``` Runs the configured `buildCommand`, packages the static output folder into a ZIP, and uploads it — without running the rest of the `rayfin up` flow. Useful for redeploying only the frontend. | Flag | Description | | --------------- | ---------------------------------------------------- | | `-v, --verbose` | Enable verbose output. | | `--skip-build` | Deploy the existing build output without rebuilding. | | `--json` | Output the result as JSON. | Requires a prior `rayfin up` (the remote endpoint must already exist) and `services.staticHosting.enabled: true` in `rayfin.yml`. ## `up functions deploy` [#up-functions-deploy] ```bash npx rayfin up functions deploy ``` > [!NOTE] > Experimental. Only registered when `services.functions.enabled: true` in `rayfin.yml` (or > `RAYFIN_FEATURE_FLAGS` includes `functions`). See > [rayfin functions](/docs/reference/cli/functions). Builds, packages, and deploys Functions to the remote Rayfin item as an escape hatch when you don't need the full `rayfin up` flow. | Flag | Description | | --------------- | --------------------------------------------------- | | `-v, --verbose` | Enable verbose output. | | `--skip-build` | Skip the build command and deploy existing content. | | `--json` | Output the result as JSON. | ## `up list` [#up-list] ```bash npx rayfin up list ``` Lists every Fabric deployment recorded in `rayfin/.deployments.json` for this project, marking the active one. Add `--json` for machine-readable output. ## `up switch` [#up-switch] ```bash npx rayfin up switch ``` Switches the active recorded deployment and rewrites `rayfin/.env` (and, unless `--no-emit-env` is passed, the framework `.env.local`) to match. `` is technically an optional positional argument — omit it when passing `--workspace-id` instead, or when passing `--list` to only inspect recorded deployments without switching. | Flag | Description | | --------------------- | -------------------------------------------------------------------- | | `--workspace-id ` | Activate by Fabric workspace ID instead of the positional name/slug. | | `-l, --list` | List recorded deployments instead of switching. | | `--no-emit-env` | Skip regenerating the framework `.env.local` after switching. | If the deployment you switch to was recorded under a different signed-in tenant, the command warns you to `rayfin login --tenant ` before deploying again. ## Agent files drift nudge [#agent-files-drift-nudge] On startup, `rayfin up` prints a one-line nudge if any Rayfin-managed agent file (`AGENTS.md`, `.mcp.json`, the Rayfin skill) is out of date, modified, missing, or newly shipped. Refresh with `rayfin init ai-files install` — see [Ai-files](/docs/reference/cli/ai-files). ```prompt title="Deploy a schema change" I added a new field to an entity under rayfin/data/. Deploy the change to Fabric with `npx rayfin up`, then run `npx rayfin up status` to confirm the deployment is healthy. ``` --- --- title: "Environment variable interpolation" description: "The ${VAR} and ${VAR:-default} syntax Rayfin supports inside rayfin.yml — usage, .env file location, resolution priority, type coercion, and error handling." url: https://rayfin.ai/docs/reference/config/env-interpolation markdown_url: https://rayfin.ai/docs/reference/config/env-interpolation.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: reference/config/env-interpolation.mdx --- # Environment variable interpolation > The ${VAR} and ${VAR:-default} syntax Rayfin supports inside rayfin.yml — usage, .env file location, resolution priority, type coercion, and error handling. `rayfin.yml` supports environment variable interpolation using `${VAR}` / `${VAR:-default}` shell-style syntax, so you can keep environment-specific values (connection strings, API keys, URLs) out of the committed config file. ## Syntax [#syntax] * `${VAR}` — simple substitution. Fails if the variable is unset or empty. * `${VAR:-default}` — substitution with a default if the variable is unset or empty. By convention, `:-` treats both an undefined variable and an empty string the same way — both fall through to the default. ```bash # .env DEFINED=value EMPTY= # UNDEFINED is not set ``` ```yaml # rayfin.yml — results: config1: ${DEFINED} # → "value" (uses the variable) config2: ${EMPTY:-fallback} # → "fallback" (empty, uses default) config3: ${UNDEFINED:-fallback} # → "fallback" (unset, uses default) config4: ${DEFINED:-fallback} # → "value" (defined, ignores default) config5: ${EMPTY} # → Error! (empty without default) config6: ${UNDEFINED} # → Error! (unset without default) ``` ## Usage [#usage] ### Basic substitution [#basic-substitution] ```yaml title="rayfin.yml" services: data: dialect: ${DB_DIALECT} ``` ```bash title="rayfin/.env" DB_DIALECT=mssql ``` ### Default values [#default-values] ```yaml title="rayfin.yml" services: data: dialect: ${DB_DIALECT:-mssql} ``` ### Partial interpolation [#partial-interpolation] Combine static text with a variable: ```yaml title="rayfin.yml" services: auth: allowedRedirectUris: - https://${APP_HOSTNAME}/callback ``` ## `.env` file location [#env-file-location] By default, Rayfin loads variables from `rayfin/.env`. Override this three ways, highest priority first: 1. **CLI flag**: ```bash npx rayfin up --env-file /production.env ``` 2. **Environment variable**: ```bash export RAYFIN_ENV_FILE='/staging.env' npx rayfin up ``` 3. **Default**: `rayfin/.env`. ## Resolution priority [#resolution-priority] 1. Shell environment variables (if non-empty). 2. Variables from the resolved `.env` file (if non-empty). 3. Default values, when specified with `:-` syntax and the variable is unset or empty. 4. Error, if the variable is unset or empty and no default is given. This is the same priority order used everywhere else Rayfin resolves environment values — see [Environment variables](/docs/reference/config/environment-variables#resolution-priority) for the canonical reference, including how it composes with `--env-file`. ## Type coercion [#type-coercion] Interpolated values are coerced to the matching YAML type **only when the entire value is a single variable reference**: ```yaml title="rayfin.yml" services: auth: expiryInMinutes: ${TOKEN_EXPIRY} # becomes the number 60, not the string "60" data: enabled: ${DATA_ENABLED} # becomes the boolean true, not the string "true" ``` ```bash title="rayfin/.env" TOKEN_EXPIRY=60 DATA_ENABLED=true ``` Partial interpolation always produces a string, even when the referenced variable looks numeric: ```yaml folder: build-${BUILD_NUMBER} # → the string "build-42" ``` ## Security best practices [#security-best-practices] 1. **Never commit `.env` files** — they hold secrets and environment-specific values. `rayfin/.env` is gitignored by default. 2. **Provide `rayfin/.env.example`** — document required variables for other developers with placeholder values. 3. **Use shell environment variables in CI/CD** — override `.env` with build and deployment secrets rather than checking a CI-specific `.env` file into the repo. 4. **Omit default values for required configuration** — a variable with no `:-` default fails fast instead of silently deploying with a wrong value. ## Error handling [#error-handling] A missing required variable fails with a specific, actionable message rather than a silent fallback: ```text ❌ Environment variable 'DB_HOST' referenced in rayfin.yml (services.data.host) is not defined. Set it in .env file or shell environment. ``` ```prompt title="Parameterize a rayfin.yml value per environment" Update my rayfin/rayfin.yml so services.data.dialect reads from a DB_DIALECT variable with mssql as the default, add DB_DIALECT to rayfin/.env.example with a comment explaining it, and confirm the actual value in rayfin/.env still resolves correctly with `npx rayfin up --dry-run`. ``` --- --- title: "Environment variables" description: "The canonical, exhaustive reference for every environment variable the Rayfin CLI and runtime read or write — frontend, tooling, feature flags, and file locations." url: https://rayfin.ai/docs/reference/config/environment-variables markdown_url: https://rayfin.ai/docs/reference/config/environment-variables.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/config/environment-variables.mdx --- # Environment variables > The canonical, exhaustive reference for every environment variable the Rayfin CLI and runtime read or write — frontend, tooling, feature flags, and file locations. This page is the single reference for every environment variable Rayfin tooling reads or writes. Other pages in this reference link here instead of repeating the tables. ## Frontend-visible variables (`RAYFIN_PUBLIC_*`) [#frontend-visible-variables-rayfin_public_] These live in `rayfin/.env` and are the **only** variables exposed to frontend builds. [`rayfin env`](/docs/reference/cli/env) (or the auto-emit built into `rayfin up`) maps them to framework-specific names in `.env.local`. | Variable | Description | Populated by | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `RAYFIN_PUBLIC_API_URL` | Deployed Fabric backend URL. | `rayfin up` | | `RAYFIN_PUBLIC_PUBLISHABLE_KEY` | Public key for Rayfin SDK initialization. | `rayfin up` | | `RAYFIN_PUBLIC_ITEM_ID` | Fabric AppBackend item ID. Used for Fabric brokered auth. | `rayfin up` | | `RAYFIN_PUBLIC_WORKSPACE_ID` | Fabric workspace ID. Used for Fabric brokered auth. | `rayfin up` | | `RAYFIN_PUBLIC_TENANT_ID` | Entra ID tenant for workspace disambiguation. | `rayfin up` | | `RAYFIN_PUBLIC_PORTAL_URL` | Fabric Portal base URL (e.g. `https://app.fabric.microsoft.com/`). | `rayfin up` | | `RAYFIN_PUBLIC_SERVICE_MODE` | `rayfin` (real backend) or `mock` (local testing). | User-set in `rayfin/.env` | | `RAYFIN_PUBLIC_FRONTEND_PORT` | Stable per-project frontend dev-server port, assigned once and reused so the dev server pins a deterministic origin the deployed backend can allow-list. | `rayfin up` | ### Framework mapping [#framework-mapping] `rayfin env --framework ` maps each `RAYFIN_PUBLIC_*` variable to a framework-specific name: | Source (`rayfin/.env`) | Vite (`.env.local`) | Next.js (`.env.local`) | Plain (`.env.local`) | | ------------------------------- | ----------------------------- | ------------------------------------ | -------------------- | | `RAYFIN_PUBLIC_API_URL` | `VITE_RAYFIN_API_URL` | `NEXT_PUBLIC_RAYFIN_API_URL` | `API_URL` | | `RAYFIN_PUBLIC_PUBLISHABLE_KEY` | `VITE_RAYFIN_PUBLISHABLE_KEY` | `NEXT_PUBLIC_RAYFIN_PUBLISHABLE_KEY` | `PUBLISHABLE_KEY` | | `RAYFIN_PUBLIC_ITEM_ID` | `VITE_FABRIC_ITEM_ID` | `NEXT_PUBLIC_FABRIC_ITEM_ID` | `ITEM_ID` | | `RAYFIN_PUBLIC_WORKSPACE_ID` | `VITE_FABRIC_WORKSPACE_ID` | `NEXT_PUBLIC_FABRIC_WORKSPACE_ID` | `WORKSPACE_ID` | | `RAYFIN_PUBLIC_TENANT_ID` | `VITE_FABRIC_TENANT_ID` | `NEXT_PUBLIC_FABRIC_TENANT_ID` | `TENANT_ID` | | `RAYFIN_PUBLIC_PORTAL_URL` | `VITE_FABRIC_PORTAL_URL` | `NEXT_PUBLIC_FABRIC_PORTAL_URL` | `PORTAL_URL` | | `RAYFIN_PUBLIC_SERVICE_MODE` | `VITE_SERVICE_MODE` | `NEXT_PUBLIC_SERVICE_MODE` | `SERVICE_MODE` | | `RAYFIN_PUBLIC_FRONTEND_PORT` | `VITE_PORT` | `PORT` | `FRONTEND_PORT` | A custom variable follows the same generic pattern: `RAYFIN_PUBLIC_FOO` becomes `VITE_RAYFIN_FOO` (Vite), `NEXT_PUBLIC_RAYFIN_FOO` (Next.js), or `FOO` (plain). `RAYFIN_PUBLIC_FRONTEND_PORT` maps to the port variable each dev server reads (`VITE_PORT` for Vite, `PORT` for Next.js); the scaffolded `vite.config.ts` pins the server to it with `strictPort`, so if the assigned port is taken the dev server fails fast instead of silently drifting to another port. To use a different port, set `RAYFIN_PUBLIC_FRONTEND_PORT` in `rayfin/.env` and re-run `rayfin env` — `rayfin up` registers whatever value is assigned in the deployed redirect allow-list. ## Tooling overrides [#tooling-overrides] Configure CLI and extension behavior. Not exposed to the frontend. Set in `rayfin/.env` or as shell environment variables. | Variable | Description | Default | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | `RAYFIN_FABRIC_API_URL` | Fabric REST API base URL the CLI calls. For `*.fabric.microsoft.com` hosts, accepts a bare origin or a full `/v1` URL (extra path segments are stripped). For non-Fabric hosts — for example, a credential proxy — accepts an origin plus path prefix; the prefix is preserved and `/v1` is appended only if not already present. | `https://api.fabric.microsoft.com/v1` | | `RAYFIN_FABRIC_PORTAL_URL` | Fabric portal base URL for deep links and `RAYFIN_PUBLIC_PORTAL_URL`. | `https://app.fabric.microsoft.com/` | | `RAYFIN_ENV_FILE` | Path to an alternate `.env` file. Equivalent to `--env-file`. | `rayfin/.env` | When set alone (not accompanied by a persisted `rayfin login`), the two Fabric endpoint variables apply only to the current process and aren't persisted — later invocations need the same shell or `rayfin/.env` value to keep using the override. Resolution precedence per variable: shell env var > value in `rayfin/.env` > persisted `environmentConfig` in `~/.rayfin/auth.json` > built-in default. > [!WARNING] > When `RAYFIN_FABRIC_API_URL` points at a non-`*.fabric.microsoft.com` host, the CLI sends > the Fabric bearer token it acquired to that host on every REST call. Only point this at a > host you trust to handle tokens responsibly, typically a first-party credential proxy you > operate yourself — there is currently no trusted-host allowlist. Operations that return a > `202` with a `Location` header are also a known limitation in proxy mode: the CLI follows > the absolute URL in `Location`, which usually points back at the upstream Fabric host and > bypasses the proxy. ## Shell-only variables [#shell-only-variables] Read from the shell environment. Never written to files. | Variable | Description | | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RAYFIN_TOKEN` | Pre-acquired bearer token for headless or non-interactive usage. Bypasses interactive Entra ID login. Prefer `rayfin login --service-principal` for CI; use `RAYFIN_TOKEN` when a token is already available from an external source (e.g. `az account get-access-token`). | | `RAYFIN_WORKSPACE_ID` | Fabric workspace ID for non-interactive setup. Used with `RAYFIN_TOKEN`. | | `RAYFIN_TENANT_ID` | Entra ID tenant used by `rayfin up` for portal URLs and the `ctid` query parameter. Equivalent to `-t, --tenant ` (precedence: flag > env var > signed-in tenant). | | `RAYFIN_ENCRYPTION_FALLBACK_ENABLED` | Set to `true` to allow a plaintext token cache on systems without OS credential storage. Development only. Equivalent to `--encryption-fallback-enabled`. | | `RAYFIN_FEATURE_FLAGS` | Comma-separated list of preview feature names to enable (case-insensitive) — see the table below. | | `RAYFIN_APPINSIGHTS_CONNECTION_STRING` | Overrides the telemetry endpoint used by the CLI and the VS Code extension. | | `RAYFIN_CONFIG_DIR` | Overrides the `~/.rayfin` directory where auth state and the token cache are stored. | | `RAYFIN_TELEMETRY_OPTOUT` | Set to `1` to disable CLI telemetry — see [Telemetry](/docs/reference/cli/telemetry). | ### Recognized `RAYFIN_FEATURE_FLAGS` values [#recognized-rayfin_feature_flags-values] | Flag | Effect | Also auto-enabled by | | ------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `storage` | Exposes the Storage prompt in `rayfin init`. | `services.storage.enabled: true` in `rayfin.yml` | | `functions` | Exposes `rayfin functions`, `rayfin up functions deploy`, and the Functions prompt in `rayfin init`. | `services.functions.enabled: true` in `rayfin.yml` | | `connectors` | Exposes the `rayfin connector` command group. | `services.connectors.enabled: true` or any entry under `connectors:` in `rayfin.yml` | Combine multiple flags with a comma, for example `RAYFIN_FEATURE_FLAGS=connectors,functions`. ## File locations [#file-locations] | Path | Purpose | Committed | | -------------------------- | ---------------------------------------------------------------------------------- | --------------- | | `rayfin/.env` | All runtime and deployment values. | No (gitignored) | | `rayfin/.env.example` | Documents expected variables with placeholder values. | Yes | | `rayfin/.deployments.json` | Multi-deployment registry (item IDs, API URLs, workspace IDs). | No (gitignored) | | `rayfin/rayfin.yml` | Project configuration, service toggles, static hosting and Functions settings. | Yes | | `rayfin/.lockfile.json` | Agent-files install record — see [Ai-files](/docs/reference/cli/ai-files). | Yes | | `.env.local` | Framework-specific frontend variables, auto-generated by `rayfin env`. | No (gitignored) | | `~/.rayfin/auth.json` | CLI authentication state (tenant, account hints, persisted environment overrides). | N/A (user home) | | `~/.rayfin/cache.bin` | Encrypted MSAL token cache (OS-backed encryption when available). | N/A (user home) | ## Resolution priority [#resolution-priority] When the same variable is defined in more than one place, the value resolves in this order (highest priority first): 1. Shell environment variable. 2. `--env-file ` CLI flag (or `RAYFIN_ENV_FILE`). 3. `rayfin/.env` file. 4. Default value (hardcoded, or from `rayfin.yml` interpolation defaults). See [Environment variable interpolation](/docs/reference/config/env-interpolation) for how `${VAR}` / `${VAR:-default}` inside `rayfin.yml` resolves against this same priority order. ```prompt title="Find why a frontend build has the wrong API URL" My built frontend is calling the wrong backend URL. Check rayfin/.env for RAYFIN_PUBLIC_API_URL, check whether a RAYFIN_PUBLIC_API_URL or VITE_RAYFIN_API_URL is set in my shell environment (which would take priority), and check .env.local. Tell me which value wins and why, then fix it by re-running `npx rayfin env --framework vite`. ``` --- --- title: "Configuration" description: "Landing page for Rayfin configuration reference — rayfin.yml schema, the exhaustive environment variable table, and ${VAR} interpolation syntax." url: https://rayfin.ai/docs/reference/config markdown_url: https://rayfin.ai/docs/reference/config.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: reference/config/index.mdx --- # Configuration > Landing page for Rayfin configuration reference — rayfin.yml schema, the exhaustive environment variable table, and ${VAR} interpolation syntax. Rayfin projects are configured through two files: `rayfin/rayfin.yml` (checked in) and `rayfin/.env` (gitignored, holds runtime values and secrets). This section is the complete reference for both. * **[`rayfin.yml` reference](/docs/reference/config/rayfin-yml)** — the full schema, key by key, with an annotated real-world example. * **[Environment variables](/docs/reference/config/environment-variables)** — every variable the CLI and runtime read or write: frontend-visible variables, tooling overrides, feature flags, and file locations. * **[Environment variable interpolation](/docs/reference/config/env-interpolation)** — `${VAR}` and `${VAR:-default}` syntax inside `rayfin.yml`, type coercion, and resolution priority. > [!TIP] > Start with [`rayfin.yml` reference](/docs/reference/config/rayfin-yml) if you're looking > at a specific config file; start with > [Environment variables](/docs/reference/config/environment-variables) if you're looking > for a specific variable name. ```prompt title="Audit a project's configuration" Read my project's rayfin/rayfin.yml and rayfin/.env.example, and tell me which services are enabled, which database dialect is configured, and whether services.auth and services.data are both declared explicitly. ``` --- --- title: "rayfin.yml reference" description: "The complete rayfin.yml schema, key by key — id, services.auth, services.data, services.storage, services.staticHosting, services.functions, and connectors." url: https://rayfin.ai/docs/reference/config/rayfin-yml markdown_url: https://rayfin.ai/docs/reference/config/rayfin-yml.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/config/rayfin-yml.mdx --- # rayfin.yml reference > The complete rayfin.yml schema, key by key — id, services.auth, services.data, services.storage, services.staticHosting, services.functions, and connectors. `rayfin/rayfin.yml` is a project's committed configuration file: which services are enabled, the database dialect, static hosting settings, and Functions settings. The CLI reads it on every command and writes back to it during scaffolding, `rayfin init` reconfiguration, and (for `allowedRedirectUris` and connector entries) after a deploy. > [!WARNING] > Always declare `services.auth` and `services.data` explicitly, even as `enabled: false`. > The CLI reads those keys without guarding and does no defaulting — omitting either block > entirely causes a failure rather than falling back to a default. Also: if > `services.data.enabled` is `true`, omitting `dialect` causes a 400 at deploy time > (`Dialect is required when Data module is enabled`). ## Top-level keys [#top-level-keys] | Key | Type | Description | | ----------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | **Required.** Unique project identifier. Used as the Fabric item name when deploying. | | `name` | string | **Required.** Human-readable project name. | | `version` | string | **Required.** Project version string. | | `services` | object | **Required.** See below. | | `connectors` | list | Optional. External Fabric data sources. Each entry has its own `name` — see [Connector](/docs/reference/cli/connector). | | `frontend` | object | Deprecated. The frontend framework is now auto-detected from `vite.config.*` / `next.config.*` / `package.json` at runtime. Retained only for backward compatibility with older `rayfin.yml` files. | | `publishable_key` | string | Legacy. Some projects scaffolded by older CLI versions carry a top-level `publishable_key`. Current CLI versions manage the publishable key through `rayfin/.env` (`RAYFIN_PUBLIC_PUBLISHABLE_KEY`) instead — see [Environment variables](/docs/reference/config/environment-variables). | ## `services.auth` [#servicesauth] | Key | Type | Description | | ----------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | `enabled` | boolean | **Required.** Turns the Auth service on or off. | | `fabric.enabled` | boolean | Fabric SSO (Entra ID). The only supported authentication method. | | `allowedRedirectUris` | string\[] | Origins allowed to receive auth redirects. `rayfin up` appends the live hosting URL here automatically after a static deploy. | | `expiryInMinutes` | number | Session token lifetime. | | `customClaims` | map | Additional claims to include in issued tokens. | | `scopes` | string\[] | Additional OAuth scopes. | | `refreshToken.lifetimeInDays` | number | Refresh token lifetime. | ## `services.data` [#servicesdata] | Key | Type | Description | | --------- | ------- | ------------------------------------------------------------------ | | `enabled` | boolean | **Required.** Turns the Data service (Data API Builder) on or off. | | `dialect` | `mssql` | **Required if `enabled` is `true`.** Fabric supports `mssql` only. | ## `services.storage` [#servicesstorage] | Key | Type | Description | | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `enabled` | boolean | Turns the Storage service (Azure Storage) on or off. Requires the `storage` feature flag to configure interactively — see [Environment variables](/docs/reference/config/environment-variables). | ## `services.staticHosting` [#servicesstatichosting] | Key | Type | Description | | --------------- | ------- | ---------------------------------------------------------------------------------------------------------------- | | `enabled` | boolean | Turns static hosting on or off. | | `folder` | string | **Required if `enabled` is `true`.** Build output directory to package and deploy, relative to the project root. | | `buildCommand` | string | Command to run before packaging (e.g. `npm run build:fabric`). | | `indexDocument` | string | Default document served for directory requests (e.g. `index.html`). | | `root` | string | Root directory of the frontend project, relative to the project root. Optional. | ## `services.functions` [#servicesfunctions] | Key | Type | Description | | -------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `enabled` | boolean | Turns the Functions service on or off. `rayfin functions init` sets this to `true` automatically when scaffolding. | | `buildCommand` | string | Command to run before deploying Functions. Optional. | ## `services.connectors` [#servicesconnectors] | Key | Type | Description | | --------- | ------- | ------------------------------------------------------------------------------------------------- | | `enabled` | boolean | Registers the `rayfin connector` command group without needing `RAYFIN_FEATURE_FLAGS=connectors`. | Set this before adding the first connector: ```yaml title="rayfin/rayfin.yml" services: connectors: enabled: true ``` ## `connectors` [#connectors] `connectors:` is a top-level **list** of entries, not a map keyed by connector name. `rayfin connector add` writes this block and keeps it in the current list shape. See [Adding a connector](/docs/connectors/adding) and [Connector authentication](/docs/connectors/auth). | Field | Type | Required | Notes | | -------------------- | ---------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------- | | `name` | string | yes | Unique. Must match `/^[a-zA-Z0-9\-_]+$/` and be at most 256 characters. | | `type` | string | yes | One of `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`, `fabric-semanticmodel`, or `kusto`. | | `version` | string | Category B only | Positive integer string, for example `'1'`. Required for `fabric-semanticmodel` and `kusto`. | | `config.workspaceId` | string | yes | Fabric workspace ID. | | `config.itemId` | string | yes | Fabric item ID. | | `auth.type` | `delegated` \| `application` | yes | Lowercase. `application` is rejected on Category B types. | | `operations[].name` | string | no | Operation objects, not bare strings. Must be a subset of the type's allowed operations; defaults to all allowed operations. | ```yaml title="rayfin/rayfin.yml" connectors: - name: sales_warehouse type: fabric-warehouse config: workspaceId: 00000000-0000-0000-0000-000000000000 itemId: 11111111-1111-1111-1111-111111111111 auth: type: delegated operations: - name: read - name: create - name: update - name: delete - name: sales_model type: fabric-semanticmodel version: '1' config: workspaceId: 00000000-0000-0000-0000-000000000000 itemId: 22222222-2222-2222-2222-222222222222 auth: type: delegated operations: - name: executeQuery ``` For `kusto`, `queryServiceUri` and `databaseName` are never written into `rayfin.yml`. They live only in the generated `rayfin/connectors//schema.ts`. ## Complete annotated example [#complete-annotated-example] This is a real, deployed project's `rayfin.yml` (from a project scaffolded with the `todoapp` template): ```yaml title="rayfin/rayfin.yml" id: test1 name: test1 version: 1.0.0 services: auth: enabled: true fabric: enabled: true # Fabric SSO — the only supported auth method allowedRedirectUris: - http://localhost:5173 # local Vite dev server - https://clear-gale-6d8b0ba024-westus.webapp.rayfingwdev.com # appended by `rayfin up` after the first static deploy data: enabled: true dialect: mssql # required whenever data.enabled is true storage: enabled: false # declared explicitly even though unused — see the warning above staticHosting: enabled: true folder: dist # Vite's build output directory buildCommand: npm run build:fabric indexDocument: index.html functions: enabled: false publishable_key: pk-atYGKKCZi3uykRKndYqX # legacy — current CLI versions keep this in rayfin/.env instead ``` ## Environment variable interpolation [#environment-variable-interpolation] Any value in `rayfin.yml` can reference an environment variable with `${VAR}` or `${VAR:-default}`: ```yaml services: data: dialect: ${DB_DIALECT:-mssql} ``` See [Environment variable interpolation](/docs/reference/config/env-interpolation) for the full syntax, type coercion rules, and resolution priority. ```prompt title="Audit required service declarations" Read my project's rayfin/rayfin.yml and confirm that both services.auth and services.data are declared explicitly, even if disabled, and that services.data.dialect is set to mssql whenever services.data.enabled is true. Fix anything missing, then run `npx rayfin up --dry-run` to confirm the change is valid before deploying for real. ``` --- --- title: "SDK" description: "Which @microsoft/rayfin-* package to install for each capability, how they depend on each other, and version notes for the whole family." url: https://rayfin.ai/docs/reference/sdk markdown_url: https://rayfin.ai/docs/reference/sdk.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/sdk/index.mdx --- # SDK > Which @microsoft/rayfin-* package to install for each capability, how they depend on each other, and version notes for the whole family. The Rayfin SDK is split into small, single-purpose packages rather than one monolithic library. Most applications only install one or two of them directly — the rest arrive as transitive dependencies. ## Which package do I need? [#which-package-do-i-need] | I want to... | Package | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | Define entities, fields, relationships, and permissions in TypeScript | [`@microsoft/rayfin-core`](/docs/reference/sdk/rayfin-core) | | Query and mutate data with a type-safe GraphQL client | [`@microsoft/rayfin-data`](/docs/reference/sdk/rayfin-data) — usually via `client.data`, rarely installed on its own | | Sign users out and manage sessions | [`@microsoft/rayfin-auth`](/docs/reference/sdk/rayfin-auth) — usually via `client.auth` | | Add Microsoft Fabric SSO | [`@microsoft/rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric) | | One configured client for data, auth, and functions | [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client) — `RayfinClient` / `RayfinServerClient` | | Call serverless functions from the client | [`@microsoft/rayfin-functions`](/docs/reference/sdk/rayfin-functions) — experimental | | Store and serve files | [`@microsoft/rayfin-storage`](/docs/reference/sdk/rayfin-storage) | | Query an existing Fabric warehouse, SQL database, semantic model, or KQL database | [`@microsoft/rayfin-connectors`](/docs/reference/sdk/rayfin-connectors) — experimental runtime kernel | | Type a Fabric warehouse, SQL database, or Lakehouse SQL analytics endpoint connector | [`@microsoft/rayfin-connector-fabric-graphql`](/docs/reference/sdk/rayfin-connector-fabric-graphql) — type-only Category A marker | | Type and run a Fabric semantic model connector | [`@microsoft/rayfin-connector-fabric-semanticmodel`](/docs/reference/sdk/rayfin-connector-fabric-semanticmodel) — DAX marker and runtime | | Type and run a Fabric KQL Database connector | [`@microsoft/rayfin-connector-kusto`](/docs/reference/sdk/rayfin-connector-kusto) — KQL marker and runtime | | *(internal)* shared HTTP client, error types, naming utilities | [`@microsoft/rayfin-lib`](/docs/reference/sdk/rayfin-lib) | In practice, most applications install two packages: ```bash npm install @microsoft/rayfin-core @microsoft/rayfin-client ``` Add `@microsoft/rayfin-auth-provider-fabric` when deploying to Fabric with Fabric SSO, and `@microsoft/rayfin-storage` or `@microsoft/rayfin-functions` when you use those services. `@microsoft/rayfin-data`, `@microsoft/rayfin-auth`, and `@microsoft/rayfin-lib` are pulled in automatically as dependencies of `@microsoft/rayfin-client` — install them directly only if you need their lower-level API without the rest of the client. ## How the packages compose [#how-the-packages-compose] ```mermaid graph TD Client["@microsoft/rayfin-client"] --> Auth["@microsoft/rayfin-auth"] Client --> Data["@microsoft/rayfin-data"] Client --> Functions["@microsoft/rayfin-functions"] Client --> Connectors["@microsoft/rayfin-connectors"] Client --> Lib["@microsoft/rayfin-lib"] AuthFabric["@microsoft/rayfin-auth-provider-fabric"] --> Auth AuthFabric --> Lib FabricGraphQL["@microsoft/rayfin-connector-fabric-graphql"] --> Connectors FabricGraphQL --> Data FabricSemantic["@microsoft/rayfin-connector-fabric-semanticmodel"] --> Connectors Kusto["@microsoft/rayfin-connector-kusto"] --> Connectors Connectors --> Core Connectors --> Data Connectors --> Lib Data --> Core["@microsoft/rayfin-core"] Data --> Lib Auth --> Lib Functions --> Lib Core --> Lib ``` `@microsoft/rayfin-core` is the odd one out in this graph: every other package is a *runtime* client that talks to your deployed backend, while `rayfin-core` is what you import in `rayfin/data/*.ts` to describe your schema at build/deploy time. `rayfin-data` depends on it only for the shared `PrimaryKeyField` type, not for any runtime behavior. ## Version notes [#version-notes] * `rayfin-core`, `rayfin-client`, `rayfin-data`, `rayfin-auth`, `rayfin-auth-provider-fabric`, `rayfin-lib`, and `rayfin-functions` are versioned in lockstep — in the environment this reference was checked against, all seven were at the same `1.31.0` release. Keep them on matching versions in your own project; mixing versions across this family is untested. * The CLI and tooling packages version independently from the SDK — `@microsoft/rayfin-cli` and `@microsoft/rayfin-docs` were at different version numbers than the SDK family in that same environment. Don't assume a CLI version implies a matching SDK version, or vice versa. * `@microsoft/rayfin-functions` and `@microsoft/rayfin-storage` are the two packages still actively evolving — the functions package is explicitly marked experimental, and both are gated behind CLI feature flags in current builds. Expect their APIs to change faster than `rayfin-core`, `rayfin-client`, `rayfin-data`, and `rayfin-auth`. * The connector packages ship in lockstep with the CLI, are preview APIs, and need version-pinned installs. Their npm `latest` and `preview` tags can lag the published release, so install the exact version printed by the connector tooling. * When in doubt about the exact signature for the version you have installed, use `rayfin docs get --symbol ` or the [MCP server](/docs/reference/cli/docs#mcp-server) rather than trusting a cached mental model of the API — see [Rules for coding agents](/docs/reference/agent-rules). ## In this section [#in-this-section] * [`@microsoft/rayfin-core`](/docs/reference/sdk/rayfin-core) — decorators for entities, fields, relationships, and permissions. * [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client) — `RayfinClient` / `RayfinServerClient` construction and configuration. * [`@microsoft/rayfin-data`](/docs/reference/sdk/rayfin-data) — the fluent query and mutation API behind `client.data`. * [`@microsoft/rayfin-auth`](/docs/reference/sdk/rayfin-auth) — the auth client behind `client.auth`. * [`@microsoft/rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric) — Fabric brokered SSO. * [`@microsoft/rayfin-functions`](/docs/reference/sdk/rayfin-functions) — typed serverless function calls. * [`@microsoft/rayfin-storage`](/docs/reference/sdk/rayfin-storage) — blob storage client. * [`@microsoft/rayfin-connectors`](/docs/reference/sdk/rayfin-connectors) — connector runtime kernel and `ConnectorsRayfinClient` support. * [`@microsoft/rayfin-connector-fabric-graphql`](/docs/reference/sdk/rayfin-connector-fabric-graphql) — type-only marker for Fabric SQL-backed entity connectors. * [`@microsoft/rayfin-connector-fabric-semanticmodel`](/docs/reference/sdk/rayfin-connector-fabric-semanticmodel) — semantic model marker, runtime, and result helpers. * [`@microsoft/rayfin-connector-kusto`](/docs/reference/sdk/rayfin-connector-kusto) — KQL Database marker, runtime, and result helpers. * [`@microsoft/rayfin-lib`](/docs/reference/sdk/rayfin-lib) — shared HTTP client and utilities. --- --- title: "@microsoft/rayfin-auth-provider-fabric" description: "Fabric brokered authentication for Rayfin apps — ensureSignedInWithFabric, the embedded and popup flows, and the origins each one works from." url: https://rayfin.ai/docs/reference/sdk/rayfin-auth-provider-fabric markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-auth-provider-fabric.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:25-07:00 source: reference/sdk/rayfin-auth-provider-fabric.mdx --- # @microsoft/rayfin-auth-provider-fabric > Fabric brokered authentication for Rayfin apps — ensureSignedInWithFabric, the embedded and popup flows, and the origins each one works from. `@microsoft/rayfin-auth-provider-fabric` lets a Rayfin app authenticate through Microsoft Fabric's brokered sign-in — the only supported sign-in method. It requires `@microsoft/rayfin-auth` (for the `Auth` instance it operates on) and `@microsoft/rayfin-lib`. > [!NOTE] > This package's catalog listing mentions MSAL, but the installed implementation does not > depend on the MSAL.js library. It implements its own PKCE (S256) + `postMessage` broker > protocol against the Fabric portal — described below. ## Installation [#installation] ```bash npm install @microsoft/rayfin-auth-provider-fabric @microsoft/rayfin-auth @microsoft/rayfin-lib ``` ## Where this works [#where-this-works] Both flows need a Rayfin backend deployed to Fabric. They differ in where your frontend has to be running: * **Popup flow** — works from any origin listed in `allowedRedirectUris`, including a local Vite dev server at `http://localhost:5173`. Your app opens the Fabric portal in a new tab; the user signs in with their Entra identity there, and the tab closes automatically once the handoff code is posted back to `returnOrigin`. * **Embedded flow** — requires your app to be loaded inside a Fabric iframe (`?fabricEmbedded=true`), where it authenticates silently via `postMessage` with no popup or user click. ## `FabricAuthOptions` [#fabricauthoptions] Every function in this package takes the same options shape: ```typescript interface FabricAuthOptions { workspaceId: string; projectId: string; fabricPortalUrl: string; returnOrigin: string; /** @deprecated backward-compat only, for pre-postMessage Fabric portals */ callbackUrl?: string; fabricEmbedded?: boolean; } ``` | Option | Type | Description | | ----------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `workspaceId` | `string` | The Fabric workspace ID. | | `projectId` | `string` | The Rayfin item ID (the AppBackend artifact ID in Fabric). | | `fabricPortalUrl` | `string` | The Fabric portal base URL, e.g. `https://app.fabric.microsoft.com`. Existing path and query parameters are preserved. | | `returnOrigin` | `string` | Your app's bare origin (e.g. `window.location.origin`) — the `postMessage` target origin for the handoff. | | `callbackUrl` | `string` | Full redirect URL for legacy, pre-`postMessage` Fabric portals. Defaults to `${returnOrigin}/auth/callback`. Deprecated — will be removed once the `postMessage` rollout completes. | | `fabricEmbedded` | `boolean` | Force embedded mode. Otherwise auto-detected from `?fabricEmbedded=true` in the URL or a prior `sessionStorage` flag. | ## `ensureSignedInWithFabric` — the primary entry point [#ensuresignedinwithfabric--the-primary-entry-point] ```typescript function ensureSignedInWithFabric(auth: Auth, options: FabricAuthOptions): Promise; ``` Implements a waterfall — the first step that succeeds short-circuits the rest: 1. Already authenticated → return the existing session. 2. A refresh token is available → attempt `auth.refreshSession()`. 3. Running inside a Fabric iframe → acquire a session via `postMessage` handoff (no popup). 4. Otherwise → open the Fabric portal in a new tab (`window.open()`) and wait for the handoff, exchanging the resulting code for a session. ```typescript import { RayfinClient } from '@microsoft/rayfin-client'; import { ensureSignedInWithFabric } from '@microsoft/rayfin-auth-provider-fabric'; const client = new RayfinClient({ baseUrl: 'https://-app.rayfin.windows.net/', publishableKey: 'pk-commonSampleAppKey', }); document.querySelector('#sign-in')?.addEventListener('click', async () => { const session = await ensureSignedInWithFabric(client.auth, { workspaceId: '', projectId: '', fabricPortalUrl: 'https://app.fabric.microsoft.com', returnOrigin: window.location.origin, }); console.log('Signed in:', session.user); }); ``` > [!WARNING] > Step 4 calls `window.open()`. Call `ensureSignedInWithFabric` from inside a synchronous > user-gesture handler (a button's `onClick`), not on page load or after an `await` — the > browser's popup blocker will otherwise block it. ## `initEmbeddedAuth` — page-load initialization [#initembeddedauth--page-load-initialization] ```typescript function initEmbeddedAuth(auth: Auth, options: FabricAuthOptions): Promise; ``` Call once at startup (for example, in a React `useEffect`). Returns `null` immediately if the app isn't running in embedded mode — it never opens a popup, so it is safe to call unconditionally on every page load. Apps that support both flows should call this on startup **and** wire `ensureSignedInWithFabric` to a sign-in button for the non-embedded case. ```typescript import { initEmbeddedAuth } from '@microsoft/rayfin-auth-provider-fabric'; const session = await initEmbeddedAuth(client.auth, fabricOptions); if (session) { console.log('Embedded session established:', session.user); } ``` > [!WARNING] > Import `@microsoft/rayfin-auth-provider-fabric` with a static `import`, not a dynamic > `import()`, in your app's entry module. The package captures the `?fabricEmbedded=true` > URL flag into `sessionStorage` as a side effect at module load — a dynamic import can run > too late, after client-side navigation has already stripped the query string. ## `initiateFabricLogin` — low-level popup only [#initiatefabriclogin--low-level-popup-only] ```typescript function initiateFabricLogin(auth: Auth, options: FabricAuthOptions): Promise; ``` Opens the Fabric portal and waits for the `postMessage` handoff, without the already-authenticated or refresh-token pre-checks that `ensureSignedInWithFabric` performs. Called internally by `ensureSignedInWithFabric`'s step 4 — most apps should use `ensureSignedInWithFabric` instead of calling this directly. ## Legacy callback bridge [#legacy-callback-bridge] ```typescript function bridgeFabricCallback(): boolean; ``` For Fabric portals that still redirect the popup to a callback page instead of using `postMessage`. Call it as early as possible on that callback page — it forwards any Fabric handoff parameters found in the URL back to the opener window (via `postMessage`, or `BroadcastChannel` when `window.opener` is unavailable) and closes the popup. Returns `true` if it handled a handoff, `false` otherwise. Deprecated — remove once your Fabric portal's redirect flow is fully retired in favor of `postMessage`. ## Other exports [#other-exports] The package root also exports two lower-level pieces that `initEmbeddedAuth` builds on, for advanced composition: ```typescript function embeddedFabricLogin(auth: Auth, options: FabricAuthOptions): Promise; function requestHandoff(params: { callbackUrl: string; codeChallenge: string; codeChallengeMethod: string; state: string; timeoutMs?: number; // default 30000 }): Promise<{ handoffCode: string; state: string }>; ``` `embeddedFabricLogin` performs a hard `auth.signOut()` before requesting the handoff, so a new embedded session never silently reuses a stale session from a previously signed-in Fabric user. Most applications should call `initEmbeddedAuth` rather than these directly. ## Errors [#errors] The package throws `AuthError` from `@microsoft/rayfin-lib` for validation and broker failures — missing required options, a blocked popup, an explicit broker error, or a handoff timeout: ```typescript import { AuthError } from '@microsoft/rayfin-lib'; try { await ensureSignedInWithFabric(client.auth, fabricOptions); } catch (error) { if (error instanceof AuthError) { console.error(error.code, error.message); } } ``` ## Security notes [#security-notes] * Every flow uses PKCE with the `S256` challenge method; the code verifier is held in a closure and never persisted to `localStorage`. * Incoming `postMessage` events are validated against `fabricPortalUrl`'s origin. * The handoff waits up to five minutes before timing out. * In embedded mode, the iframe's own `localStorage` holds the session, isolated from the parent frame by the browser's same-origin policy. ## Environment variables [#environment-variables] `npx rayfin up` writes `RAYFIN_PUBLIC_ITEM_ID`, `RAYFIN_PUBLIC_WORKSPACE_ID`, and `RAYFIN_PUBLIC_PORTAL_URL` to `rayfin/.env`; `rayfin env --framework vite` maps them to `VITE_FABRIC_ITEM_ID`, `VITE_FABRIC_WORKSPACE_ID`, and `VITE_FABRIC_PORTAL_URL` in `.env.local`, which map to `projectId`, `workspaceId`, and `fabricPortalUrl` respectively. See [`rayfin.yml` auth configuration](/docs/reference/config/rayfin-yml) and [Fabric SSO](/docs/auth/fabric-sso). ## Troubleshooting [#troubleshooting] | Symptom | Likely cause | Fix | | --------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Popup blocked | Called outside a synchronous user-gesture handler | Call `ensureSignedInWithFabric` directly from a button's `onClick`. | | Session not persisting | `RayfinClient` misconfigured | Confirm `baseUrl` and `publishableKey` are correct. | | Times out after 5 minutes | Handoff code never received | Confirm `returnOrigin` matches your app's actual origin. | | Origin mismatch | Wrong portal URL for the environment | Verify `fabricPortalUrl` matches the Fabric portal you're actually using (production, PPE, dev). | | `initEmbeddedAuth` returns `null` | Not detected as embedded | Ensure the URL has `?fabricEmbedded=true`, or set `fabricEmbedded: true` explicitly. | | Embedded handoff timeout | Parent frame didn't respond | Verify `returnOrigin` matches the iframe's actual origin. | | State mismatch error | Replayed or stale response | Retry the flow from scratch; treat as a potential replay attempt if it recurs. | See [Errors and troubleshooting](/docs/reference/errors) for the full site-wide index. ## Browser requirements [#browser-requirements] This package targets browsers — it uses `window.open()`, `postMessage`, `BroadcastChannel`, and `window.location`. It is not intended for Node.js or server-side use. --- --- title: "@microsoft/rayfin-auth" description: "The Auth client surface — signOut, session management, and the OpaqueSession shape — with exact signatures from the SDK." url: https://rayfin.ai/docs/reference/sdk/rayfin-auth markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-auth.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: reference/sdk/rayfin-auth.mdx --- # @microsoft/rayfin-auth > The Auth client surface — signOut, session management, and the OpaqueSession shape — with exact signatures from the SDK. `@microsoft/rayfin-auth` implements Rayfin's authentication client: session lifecycle management, token refresh, and sign-out. You normally reach it through `client.auth` on a [`RayfinClient`](/docs/reference/sdk/rayfin-client) rather than constructing it yourself. Signing in happens through [`@microsoft/rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric), which operates on this same `Auth` instance — Fabric SSO (Entra ID) is the only supported sign-in method. ## Installation [#installation] ```bash npm install @microsoft/rayfin-auth ``` ## The `Auth` class [#the-auth-class] ```typescript class Auth { constructor(apiClient: ApiClient, options?: { storage?: AuthStorage | boolean; storageKeyPrefix?: string }); } interface AuthStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; clear(): void; } ``` Rayfin's own SDK auto-detects Node.js, React Native, and Electron, skipping browser-only APIs (like `localStorage`) when `window` is undefined — the isomorphic design means the same `Auth` code runs in any of these environments without crashing. ## Signing out [#signing-out] | Method | Signature | Description | | ------------ | ----------------------------------- | ----------------------------------------------------------------------------- | | `signOut` | `() => Promise` | Revokes the current access token and clears the local session. | | `signOutAll` | `() => Promise` | Revokes every active session for the user (all devices). Returns `{ count }`. | ```typescript await auth.signOut(); ``` ## Session management [#session-management] | Method | Signature | Description | | ----------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `getSession` | `() => OpaqueSession` | Returns the current session **synchronously** — not a `Promise`. | | `onSessionChange` | `(callback: (session: OpaqueSession \| null) => void) => () => void` | Subscribes to session changes; returns an unsubscribe function. | | `hasRefreshToken` | `() => boolean` | Whether a refresh token is available for `refreshSession()`. | | `refreshSession` | `() => Promise` | Refreshes using the stored refresh token. Concurrent calls share one in-flight request. | ```typescript const session = auth.getSession(); // no await — synchronous if (session.isAuthenticated) { console.log(session.user?.email); } const unsubscribe = auth.onSessionChange((session) => { setCurrentSession(session); }); // later: unsubscribe(); ``` > [!WARNING] > The session-change callback is **`onSessionChange`**. `onAuthStateChange` does not exist > on the Rayfin auth client — see [Known limitations](/docs/reference/known-limitations). ### Session shape [#session-shape] Session objects are opaque by design — gate UI logic on `isAuthenticated` or the presence of `user`, not on internal fields. ```typescript interface OpaqueSession { user: User | null; role?: string; expiresAt?: Date; isAuthenticated: boolean; isAnonymous: boolean; } interface User { id: string; email: string; role?: string; emailVerified?: boolean; emailVerifiedAt?: string | null; } ``` ## Verifying tokens [#verifying-tokens] | Method | Signature | Description | | --------- | ----------------------------- | ------------------------------------------------------------------------------------------- | | `getJwks` | `() => Promise` | Public keys for verifying Rayfin-issued JWTs, for services that validate tokens themselves. | ## Events [#events] `on(event, handler)` subscribes to a specific named `AuthEvent` (in addition to the general `onSessionChange`), returning an unsubscribe function. Events relevant to session lifecycle — independent of how the user signed in — include `'AUTH_LOGIN'`, `'AUTH_LOGOUT'`, `'AUTH_REFRESH'`, and `'AUTH_SESSION_EXPIRED'`: ```typescript function on(event: AuthEvent, handler: (session: OpaqueSession) => void): () => void; auth.on('AUTH_SESSION_EXPIRED', () => redirectToLogin()); ``` ## React usage [#react-usage] ```typescript import { useState, useEffect } from 'react'; import { auth } from './lib/rayfin'; import type { OpaqueSession } from '@microsoft/rayfin-auth'; export function useAuth() { const [session, setSession] = useState(null); useEffect(() => { setSession(auth.getSession()); return auth.onSessionChange(setSession); }, []); return { session, isAuthenticated: session?.isAuthenticated ?? false, signOut: auth.signOut.bind(auth), }; } ``` Wire up sign-in separately with [`ensureSignedInWithFabric`](/docs/reference/sdk/rayfin-auth-provider-fabric), which takes this same `auth` instance. ## Configuration and restart behavior [#configuration-and-restart-behavior] Auth is configured in `rayfin.yml` under `services.auth` (`enabled`, `allowedRedirectUris`, `fabric.enabled`). After changing any of these values, restart the backend (`npx rayfin up`) so the updated endpoints are exposed — the running server does not pick up `rayfin.yml` changes on its own. See [Known limitations](/docs/reference/known-limitations). --- --- title: "@microsoft/rayfin-client" description: "RayfinClient construction, configuration options, and the client.data, client.auth, and client.functions facades, with exact signatures from the SDK." url: https://rayfin.ai/docs/reference/sdk/rayfin-client markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-client.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:51:33-07:00 source: reference/sdk/rayfin-client.mdx --- # @microsoft/rayfin-client > RayfinClient construction, configuration options, and the client.data, client.auth, and client.functions facades, with exact signatures from the SDK. `@microsoft/rayfin-client` is the main SDK entrypoint. It composes [`@microsoft/rayfin-auth`](/docs/reference/sdk/rayfin-auth), [`@microsoft/rayfin-data`](/docs/reference/sdk/rayfin-data), and [`@microsoft/rayfin-functions`](/docs/reference/sdk/rayfin-functions) behind a single configured client, built on the HTTP plumbing in [`@microsoft/rayfin-lib`](/docs/reference/sdk/rayfin-lib). ## Installation [#installation] ```bash npm install @microsoft/rayfin-client ``` ## `RayfinClient` [#rayfinclient] Use `RayfinClient` in browser and frontend code — it includes the full `auth` facade. ```typescript class RayfinClient< TSchema extends EntitySchema = Record, TFunctionsSchema extends FunctionsSchema = FunctionsSchema, > { readonly data: TypedDataClients; readonly auth: Auth; readonly functions: TypedFunctionClients; constructor(config: RayfinClientConfig); } ``` The first type parameter maps entity names to their classes, so `client.data.` is fully typed. Pass your `AppSchema` (built from `rayfin/data/schema.ts`) and, if you use `@microsoft/rayfin-functions`, your `FunctionsSchema`. ```typescript title="src/services/rayfinClient.ts" import { RayfinClient } from '@microsoft/rayfin-client'; import type { AppSchema } from '../../rayfin/data/schema'; const client = new RayfinClient({ baseUrl: 'https://-app.rayfin.windows.net/', publishableKey: 'pk-commonSampleAppKey', }); const todos = await client.data.Todo.select(['id', 'title']).execute(); ``` ### `RayfinClientConfig` [#rayfinclientconfig] ```typescript interface RayfinClientConfig extends ApiClientConfig { authStorage?: AuthStorage | boolean; } interface ApiClientConfig { baseUrl: string; publishableKey: string; headers?: Record; timeout?: number; getAccessToken?: () => string | null; useProxy?: boolean; onRefreshNeeded?: () => Promise; } ``` | Option | Type | Default | Description | | ----------------- | ------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `baseUrl` | `string` | — | Required. The Rayfin backend's base URL. | | `publishableKey` | `string` | — | Required. The project's public `pk-*` key. Safe for client-side code. | | `authStorage` | `AuthStorage \| boolean` | `true` (browser) | `true` uses `localStorage`; `false` disables persistence (useful in Node.js scripts); or pass a custom object implementing `getItem`/`setItem`/`removeItem`/`clear`. | | `headers` | `Record` | none | Extra headers sent with every request. | | `timeout` | `number` | none | Request timeout in milliseconds. | | `getAccessToken` | `() => string \| null` | wired up by `Auth` | Overrides how the client obtains an access token for the `Authorization` header. | | `useProxy` | `boolean` | `true` | Under a Vite dev server, rewrites `baseUrl` to a relative path so requests go through Vite's dev proxy instead of hitting the absolute URL directly (avoids local CORS issues). Set `false` to always use the literal `baseUrl`. | | `onRefreshNeeded` | `() => Promise` | wired up by `Auth` | Called before a request is retried after a `401`. | `getAccessToken` and `onRefreshNeeded` are normally wired up automatically when `RayfinClient` constructs its internal `Auth` instance — most applications never set them directly. ## `RayfinServerClient` [#rayfinserverclient] Use `RayfinServerClient` in Node.js and worker code. It skips the browser-coupled `Auth` module entirely; you supply the access token yourself. ```typescript class RayfinServerClient> { readonly data: TypedDataClients; constructor(config: RayfinServerClientConfig); } interface RayfinServerClientConfig extends Omit { accessToken?: string | (() => string | null); } ``` ```typescript import { RayfinServerClient } from '@microsoft/rayfin-client'; import type { AppSchema } from '../rayfin/data/schema'; const client = new RayfinServerClient({ baseUrl: process.env.RAYFIN_BASE_URL!, publishableKey: process.env.RAYFIN_PUBLISHABLE_KEY!, accessToken: () => incomingRequest.headers.authorization, }); const todos = await client.data.Todo.select(['id', 'title']).execute(); ``` `accessToken` accepts either a static string or a function, so a server can rotate the token it forwards per request (for example, from an incoming request's `Authorization` header). ## The `data` facade [#the-data-facade] Both client classes expose `.data`, typed as `TypedDataClients` — one `GraphQLEntityClient` per entry in `TSchema`, giving you `client.data..select()`, `.where()`, `.create()`, `.update()`, `.delete()`, and more. See [`@microsoft/rayfin-data`](/docs/reference/sdk/rayfin-data) for the full query and mutation API. ## The `auth` facade [#the-auth-facade] `RayfinClient.auth` is a full `Auth` instance from `@microsoft/rayfin-auth` — `signOut`, `onSessionChange`, `getSession`, and the rest of the session lifecycle. Sign-in itself goes through [`@microsoft/rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric), which operates on this same `Auth` instance. See [`@microsoft/rayfin-auth`](/docs/reference/sdk/rayfin-auth) for the complete surface. `RayfinServerClient` has no `auth` property, since server code authenticates via the `accessToken` config option instead of a browser session. ## The `functions` facade [#the-functions-facade] `RayfinClient.functions` is typed as `TypedFunctionClients` — one `FunctionClient` per entry in your `FunctionsSchema`, each with a typed `invoke()`. See [`@microsoft/rayfin-functions`](/docs/reference/sdk/rayfin-functions). ## Errors [#errors] Both client classes expose a static `errors` map, and instances throw these types instead of raw `Error`: ```typescript class RayfinClientBase { static readonly errors: { SdkError: typeof SdkError; AuthError: typeof AuthError; NetworkError: typeof NetworkError; }; } ``` `SdkError` and `NetworkError` in this map are re-exported directly from `@microsoft/rayfin-lib`. `AuthError` is **not** — `@microsoft/rayfin-client` declares its own `AuthError extends SdkError`, distinct from (though structurally identical to) `@microsoft/rayfin-lib`'s own `AuthError` class. Import whichever one matches where the error actually originated; `instanceof` checks against the wrong package's `AuthError` will not match. Catch by branching on failure type: ```typescript import { RayfinClient } from '@microsoft/rayfin-client'; try { await client.auth.refreshSession(); } catch (error) { if (error instanceof RayfinClient.errors.AuthError) { console.error('Auth failed:', error.message); } else if (error instanceof RayfinClient.errors.NetworkError) { console.error('Network issue:', error.message); } } ``` > [!NOTE] > Newer releases of `@microsoft/rayfin-client` add `setDeprecationsSilenced()` and > `isDeprecationSilenced()` for quieting deprecation warnings in application code. See > [Deprecation warnings](/docs/reference/deprecations) for usage and the version this > applies to. --- --- title: "@microsoft/rayfin-connector-fabric-graphql" description: "Type-only marker APIs for Category A Fabric SQL connectors that expose typed entity CRUD through client.connectors." url: https://rayfin.ai/docs/reference/sdk/rayfin-connector-fabric-graphql markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-connector-fabric-graphql.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/sdk/rayfin-connector-fabric-graphql.mdx --- # @microsoft/rayfin-connector-fabric-graphql > Type-only marker APIs for Category A Fabric SQL connectors that expose typed entity CRUD through client.connectors. `@microsoft/rayfin-connector-fabric-graphql` supplies TypeScript-only markers for Category A connectors. It has no runtime behavior; the entity proxy comes from [`@microsoft/rayfin-connectors`](/docs/reference/sdk/rayfin-connectors). See [Generating entity files](/docs/connectors/entity-generation) for the guide workflow. > [!WARNING] > Connectors are in private preview. This API may change between releases. ## Installation [#installation] ```bash npm install @microsoft/rayfin-connector-fabric-graphql@1.36.0-alpha ``` This package is not a dependency of `@microsoft/rayfin-client`, so a fresh app is missing it until you install it explicitly. Pin the version to the Rayfin CLI release you are using. ## `GraphQLBackedConnector` [#graphqlbackedconnector] `GraphQLBackedConnector` is the published marker for all Category A connector types: `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase`. Per-type marker names such as `FabricWarehouse` do not exist. ```typescript import type { ConnectorConfig } from '@microsoft/rayfin-connectors'; import type { GraphQLBackedConnector } from '@microsoft/rayfin-connector-fabric-graphql'; import { Customer } from './Customer'; import { Order } from './Order'; export const connectorConfig = { connector: 'fabric-warehouse', operations: ['read', 'create', 'update', 'delete'], entities: { Customer: ['CustomerId', 'Name'], Order: ['OrderId', 'CustomerId', 'Amount'], }, } as const satisfies ConnectorConfig; export type SalesWarehouseConnector = GraphQLBackedConnector< { Customer: typeof Customer; Order: typeof Order; }, typeof connectorConfig >; ``` `TSchema` maps entity names to their classes with `typeof`. The constructor type carries the primary-key phantom from the entity source metadata; a bare instance type is treated as keyless. `TConfig` should be `typeof connectorConfig`, with the value declared `as const satisfies ConnectorConfig` so the `connector` and `operations` literals are not widened. ## Restricted CRUD surface [#restricted-crud-surface] The marker resolves to a `RestrictedDataApi`, one `RestrictedEntityClient` per entity: ```typescript type RestrictedDataApi< TSchema extends EntitySchema, TOps extends CrudOperation = CrudOperation, TDialect extends ConnectorType = ConnectorType, > = { [K in keyof TSchema & string]: RestrictedEntityClient; }; type RestrictedEntityClient< TSchema extends EntitySchema, TEntity extends keyof TSchema & string, TOps extends CrudOperation, TDialect extends ConnectorType = ConnectorType, > = Pick, AllowedMethods>; ``` `CrudOperation` is re-exported from `@microsoft/rayfin-connectors`. The marker reads `connectorConfig.operations` and keeps only the methods mapped to those CRUD verbs. It also drops `findByKey`, `update`, and `delete` for entities that do not declare a usable primary key. ## Runtime dependency [#runtime-dependency] The marker gates methods at compile time only. Runtime routing, GraphQL execution, read-after-write behavior, and `OPERATION_NOT_ALLOWED` checks live in `@microsoft/rayfin-connectors`, mounted by `ConnectorsRayfinClient` from `@microsoft/rayfin-client/experimental`. Link the generated connector schema into that client as described in [Wiring connectors into your app](/docs/connectors/client-setup). --- --- title: "@microsoft/rayfin-connector-fabric-semanticmodel" description: "Marker, runtime, direct execution, URL parsing, Arrow decoding, and normalized result APIs for Fabric semantic model connectors." url: https://rayfin.ai/docs/reference/sdk/rayfin-connector-fabric-semanticmodel markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-connector-fabric-semanticmodel.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/sdk/rayfin-connector-fabric-semanticmodel.mdx --- # @microsoft/rayfin-connector-fabric-semanticmodel > Marker, runtime, direct execution, URL parsing, Arrow decoding, and normalized result APIs for Fabric semantic model connectors. `@microsoft/rayfin-connector-fabric-semanticmodel` provides the type marker and runtime helpers for `fabric-semanticmodel` connectors. Use it with `ConnectorsRayfinClient` from `@microsoft/rayfin-client/experimental`. See [Semantic models](/docs/connectors/semantic-models) for guide-level usage. > [!WARNING] > Connectors are in private preview. This API may change between releases. ## Installation [#installation] ```bash npm install @microsoft/rayfin-connector-fabric-semanticmodel@1.36.0-alpha ``` ## Marker and operation catalog [#marker-and-operation-catalog] `FabricSemanticModel` is the connector marker. It defaults to the full operation union, currently only `executeQuery`. ```typescript import type { OperationDef } from '@microsoft/rayfin-connectors'; import type { ExecuteQueryInput, FabricSemanticModel, SemanticModelQueryResult, } from '@microsoft/rayfin-connector-fabric-semanticmodel'; interface FabricSemanticModelOperationCatalog { executeQuery: OperationDef; } type AppConnectorsSchema = { salesModel: FabricSemanticModel<'executeQuery'>; }; ``` The `executeQuery` output is the normalized `SemanticModelQueryResult` union. The runtime folds the wire envelope inside its `invoke` middleware before the caller receives it. ## Query input [#query-input] ```typescript interface ExecuteQueryInput { query: string; resultSetRowCountLimit?: number; } ``` `query` is the DAX text. `resultSetRowCountLimit` is optional and has no default limit in the input shape; when present on a runtime-processed call, it overrides the runtime option for that call. ## Normalized result [#normalized-result] ```typescript type SemanticModelQueryResult = | { status: 'success'; table: QueryTable; requestId: string; } | { status: 'error'; error: QueryError; requestId: string; }; interface QueryTable { columns: QueryColumn[]; rows: unknown[][]; } interface QueryColumn { name: string; dataType: string; } interface QueryError { category: 'api' | 'query' | 'network' | 'overflow' | 'unknown'; message: string; code?: string; details?: string; recoveryHint?: string; } function toQueryResult( response: FabricSemanticModelTabularResponse | SemanticModelQueryResult, ): SemanticModelQueryResult; ``` `toQueryResult` accepts the raw tabular envelope or an already-normalized result. It returns success rows as column-aligned arrays and gives failures a category and message. ## Runtime options [#runtime-options] `fabricSemanticModel(options?)` returns the `ConnectorRuntime` registered under the same connector name passed to `ConnectorsRayfinClient`. ```typescript function fabricSemanticModel(options?: FabricSemanticModelOptions): ConnectorRuntime; interface FabricSemanticModelOptions { target?: FabricSemanticModelTarget | (() => FabricSemanticModelTarget | undefined); baseUrl?: string; endpoints?: FabricEndpoints; getToken?: () => string | undefined | Promise; sessionId?: string; culture?: string; schemaOnly?: boolean; queryTimeout?: number; resultSetRowCountLimit?: number; } ``` `target`, `baseUrl`, `endpoints`, `getToken`, and `sessionId` are used by the direct CLI path. `culture`, `schemaOnly`, `queryTimeout`, and `resultSetRowCountLimit` become DAX query options. ## URLs and endpoints [#urls-and-endpoints] `parseFabricUrl(url)` parses a portal URL into `{ workspaceId, itemId, itemType }`. `parseSemanticModelUrl(url)` does the same and rejects URLs that do not address a semantic model. ```text https://app.fabric.microsoft.com/groups/{workspaceId}/semanticmodels/{itemId} https://app.powerbi.com/groups/{workspaceId}/modeling/{itemId} https://app.powerbi.com/onelake/details/{workspaceId}/dataset/{itemId} ``` Endpoint helpers are exported for runtime configuration: ```typescript const DEFAULT_ENDPOINTS: FabricEndpoints; const DEFAULT_POWER_BI_BASE_URL: string; function derivePowerBiBaseUrl(endpoints?: FabricEndpoints): string; ``` `DEFAULT_ENDPOINTS.fabricApi` is `https://api.fabric.microsoft.com/v1`. `DEFAULT_POWER_BI_BASE_URL` is `https://api.powerbi.com/v1.0/myorg`. ## Direct execution helpers [#direct-execution-helpers] ```typescript function executeDaxDirect( http: InvokeHttpClient, target: FabricSemanticModelTarget, query: string, options?: FabricSemanticModelRuntimeOptions, ): Promise; function resolveTarget( target: FabricSemanticModelRuntimeOptions['target'], ): FabricSemanticModelTarget | undefined; function resolveBaseUrl( options: Pick, ): string; function toNetworkErrorResponse( err: unknown, requestId: string, ): FabricSemanticModelTabularResponse; ``` `executeDaxDirect` never throws. Non-2xx responses, network failures, and parse failures all become a response with `status: 'Failed'`. ## Arrow decoding [#arrow-decoding] ```typescript function parseArrowStream( bytes: ArrayBuffer | Uint8Array, requestId?: string, ): FabricSemanticModelTabularResponse; class ArrowOverflowError extends Error {} ``` `parseArrowStream` decodes Apache Arrow IPC streams into the tabular envelope. It maps DAX error tables to `queryError`, represents unsafe numeric coercions as table errors, and preserves column metadata when the stream provides it. --- --- title: "@microsoft/rayfin-connector-kusto" description: "Marker, runtime, config, raw response, and normalization APIs for Fabric KQL Database connectors." url: https://rayfin.ai/docs/reference/sdk/rayfin-connector-kusto markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-connector-kusto.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/sdk/rayfin-connector-kusto.mdx --- # @microsoft/rayfin-connector-kusto > Marker, runtime, config, raw response, and normalization APIs for Fabric KQL Database connectors. `@microsoft/rayfin-connector-kusto` provides both the `kusto` connector marker and its runtime helper. Use it with `ConnectorsRayfinClient` from `@microsoft/rayfin-client/experimental`. See [KQL databases](/docs/connectors/kusto) for guide-level usage. > [!WARNING] > Connectors are in private preview. This API may change between releases. ## Installation [#installation] ```bash npm install @microsoft/rayfin-connector-kusto@1.36.0-alpha ``` This package supplies both the marker and the config type, so `kusto` connectors need no separate runtime package. ## Marker and operation catalog [#marker-and-operation-catalog] `Kusto` is the connector marker. It defaults to both operations: `executeQuery` and `executeCommand`. ```typescript import type { OperationDef } from '@microsoft/rayfin-connectors'; import type { ExecuteCommandInput, ExecuteQueryInput, Kusto, KustoCommandResponse, KustoQueryResponse, } from '@microsoft/rayfin-connector-kusto'; interface KustoOperationCatalog { executeQuery: OperationDef; executeCommand: OperationDef; } type AppConnectorsSchema = { telemetry: Kusto<'executeQuery' | 'executeCommand'>; }; ``` The operation outputs are the raw native Kusto v1 `{ Tables }` document, unlike the semantic model package's normalized output. Normalize query or command responses with `toQueryResult`. ## Inputs and config [#inputs-and-config] ```typescript interface ExecuteQueryInput { query: string; clientRequestId?: string; } interface ExecuteCommandInput { command: string; clientRequestId?: string; } interface KustoConnectorConfig extends ConnectorConfig { connector: 'kusto'; queryServiceUri: string; databaseName: string; } ``` `KustoConnectorConfig` is exported from `@microsoft/rayfin-connector-kusto`, not from `@microsoft/rayfin-connectors`. `queryServiceUri` and `databaseName` are connector-owned routing values; callers pass only `query` or `command` plus an optional correlation id. ## Runtime [#runtime] `kusto()` returns a `ConnectorRuntime`: ```typescript function kusto(): ConnectorRuntime; ``` It registers `invoke` middleware for `executeQuery` and `executeCommand`. The middleware merges `queryServiceUri` and `databaseName` into the outbound payload after caller input, so a caller cannot override the connector's resolved route. It also generates a `clientRequestId` when the caller omits one and Web Crypto is available. ## Raw response and normalization [#raw-response-and-normalization] ```typescript interface KustoQueryResponse { Tables?: KustoV1Table[]; } type KustoCommandResponse = KustoQueryResponse; interface KustoV1Table { TableName?: string; Columns?: KustoV1Column[]; Rows?: unknown[]; } interface KustoV1Column { ColumnName?: string; ColumnType?: string; DataType?: string; } function toQueryResult( response: KustoQueryResponse, correlation?: KustoCorrelation, ): KustoQueryResult; ``` `toQueryResult` transforms the native Kusto v1 response into a discriminated result: ```typescript type KustoQueryResult = | { status: 'success'; tables: KustoTable[]; clientRequestId: string; activityId?: string; } | { status: 'error'; error: KustoQueryError; clientRequestId: string; activityId?: string; }; interface KustoTable { name: string; columns: KustoColumn[]; rows: unknown[][]; } interface KustoColumn { name: string; type: string; } interface KustoCorrelation { clientRequestId?: string; activityId?: string; } interface KustoQueryError { message: string; code?: string; } ``` `clientRequestId` and `activityId` are out-of-band correlation values. Pass what you know to `toQueryResult`; they are not read from the native response body. --- --- title: "@microsoft/rayfin-connectors" description: "Connector runtime APIs for mounting typed Fabric SQL, semantic model, and KQL Database connectors on the Rayfin client." url: https://rayfin.ai/docs/reference/sdk/rayfin-connectors markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-connectors.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/sdk/rayfin-connectors.mdx --- # @microsoft/rayfin-connectors > Connector runtime APIs for mounting typed Fabric SQL, semantic model, and KQL Database connectors on the Rayfin client. `@microsoft/rayfin-connectors` is the runtime kernel behind `client.connectors`. It creates the typed connector proxy, chooses the transport for each connector category, and defines the shared operation, error, host, and middleware types. See the [Connectors guide](/docs/connectors) for concepts and setup flow. > [!WARNING] > Connectors are in private preview. This API may change between releases. ## Installation [#installation] ```bash npm install @microsoft/rayfin-connectors@1.36.0-alpha ``` `@microsoft/rayfin-client` depends on this package, so it usually arrives transitively. Declare it directly when application code imports its types or helpers, because strict resolvers such as pnpm treat undeclared transitive imports as phantom dependencies. ## `createConnectorsApi` [#createconnectorsapi] `createConnectorsApi` builds the proxy mounted as `client.connectors`: ```typescript function createConnectorsApi( apiClient: ApiClient, configs: Record, runtime?: ConnectorsRuntime, host?: HostEnvironment, ): TypedConnectorsApi; ``` The dispatcher reads `configs[name].connector` when a connector is first accessed. The Category A types `fabric-sqlanalytics`, `fabric-warehouse`, and `fabric-sqldatabase` get a `GraphQLConnectorClient`, so the surface is `client.connectors..`. All other connector types get a `SemanticConnectorClient`, so the surface is `client.connectors..(input, options?)`. Accessing a missing connector configuration throws `ConnectorsError` with code `UNKNOWN_CONNECTOR`. ## `ConnectorsRayfinClient` [#connectorsrayfinclient] Most applications reach `createConnectorsApi` through `ConnectorsRayfinClient`, exported from `@microsoft/rayfin-client/experimental`. It is not exported from the stable `@microsoft/rayfin-client` entry. ```typescript import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental'; const client = new ConnectorsRayfinClient< AppSchema, AppFunctionsSchema, AppConnectorsSchema >( { baseUrl: import.meta.env.VITE_RAYFIN_API_URL, publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY, connectors: { salesDb: salesDbConnectorConfig, salesModel: salesModelConnectorConfig, }, }, { salesModel: fabricSemanticModel(), }, ); ``` Its constructor is: ```typescript class ConnectorsRayfinClient< TSchema extends EntitySchema = Record, TFunctionsSchema extends FunctionsSchema = FunctionsSchema, TConnectorsSchema extends ConnectorsSchema = ConnectorsSchema, > extends RayfinClient { readonly connectors: TypedConnectorsApi; constructor( config: ConnectorsRayfinClientConfig, connectorsRuntime?: ConnectorsRuntime, ); } interface ConnectorsRayfinClientConfig< TConnectorsSchema extends ConnectorsSchema = ConnectorsSchema, > extends RayfinClientConfig { connectors: Record; host?: HostEnvironment; } ``` `connectors` is required and exhaustive for the connector schema. `host` is optional; when it is absent the connector layer calls `detectHost()`. ## Schema and operation types [#schema-and-operation-types] The package exports the shared type vocabulary consumed by connector marker packages: ```typescript type ConnectorsSchema = Record; interface ConnectorMarker { readonly __client?: TClient; } interface ConnectorConfig { connector: ConnectorType; operations?: readonly CrudOperation[]; entities?: Record; } interface OperationDef { readonly __input?: TInput; readonly __output?: TOutput; } type OperationCatalog = Record>; type TypedConnectorClient = { [K in keyof TCatalog & string]: (input: unknown, options?: InvokeOptions) => Promise; }; type TypedConnectorsApi = { [K in keyof TSchema & string]: unknown; }; interface InvokeOptions { headers?: Record; } ``` `ConnectorType` is the connector type union shared with the CLI, and `CrudOperation` is the Category A verb union. `ConnectorConfig.operations` gates Category A CRUD methods at runtime; `ConnectorConfig.entities` supplies default selections and relationship metadata for entity connectors. ## Host detection [#host-detection] ```typescript interface HostEnvironment { type: 'embedded' | 'standalone' | 'cli'; } function detectHost(): HostEnvironment; ``` `detectHost()` returns `'cli'` when there is no browser DOM and `'standalone'` in browsers. It never auto-detects `'embedded'`; a host package must assert that environment by passing an explicit `host`. ## Category A entity clients [#category-a-entity-clients] `ConnectorEntityClient` is the entity surface behind Category A connectors. It supports the read chain `select`, `where`, `orderBy`, and `first`, terminal reads `findMany`, `findFirst`, and `findByKey`, mutations `create`, `update`, and `delete`, and the aggregation entry points `groupBy` and `aggregate`. See [Fabric SQL sources](/docs/connectors/sql-sources) for usage patterns and [Aggregations](/docs/data/aggregations) for aggregation shapes. CRUD operations map to client methods through `METHODS_FOR_CRUD_OPERATION`: | CRUD operation | Methods | | -------------- | --------------------------------------------------------------------------------------------------- | | `read` | `select`, `where`, `orderBy`, `first`, `groupBy`, `aggregate`, `findMany`, `findFirst`, `findByKey` | | `create` | `create` | | `update` | `update` | | `delete` | `delete` | `MutationResult` returns the row for connectors that support read-after-write. Fabric Warehouse mutations instead return `DbOperationResult`: ```typescript interface DbOperationResult { readonly result: string; } ``` Warehouse uses this `{ result: string }` shape because that dialect does not read the mutated row back after the operation. ## Errors [#errors] Connector failures use a shared result contract: ```typescript type ConnectorErrorCategory = 'network' | 'api' | 'query' | 'overflow' | 'unknown'; interface ConnectorError { message: string; code?: string; category?: ConnectorErrorCategory; details?: string; recoveryHint?: string; } interface ConnectorErrorResult { status: 'error'; error: ConnectorError; } function isConnectorError(value: unknown): value is ConnectorErrorResult; class ConnectorsError extends SdkError { name: 'ConnectorsError'; constructor(message: string, code?: string); } ``` `isConnectorError` narrows normalized operation results that satisfy the shared failed shape. `ConnectorsError` is thrown by SDK-side routing and validation failures, such as an unknown connector or a gated CRUD method. ## Runtime middleware [#runtime-middleware] Connector-specific packages add behavior with `ConnectorRuntime`: ```typescript type ConnectorsRuntime = Record; interface ConnectorRuntime { operations?: Record; } interface OperationRuntime { decodeBinary?: (data: ArrayBuffer) => unknown; invoke?: (ctx: InvokeContext, next: InvokeNext) => Promise; } interface InvokeContext { readonly connectorName: string; readonly operation: string; readonly input?: unknown; readonly options?: InvokeOptions; readonly host: HostEnvironment; readonly connectorConfig?: ConnectorConfig; readonly http?: InvokeHttpClient; } type InvokeNext = (ctx: InvokeContext) => Promise; interface InvokeHttpClient { fetch(input: Request | string | URL, init?: RequestInit): Promise; } ``` `invoke` can handle a call itself or call `next(ctx)` to use the default transport. `decodeBinary` runs only for binary payloads returned through the operation proxy. > [!NOTE] > `client.connectors..invoke('op', input)` is a raw escape hatch. It uses the > default standalone transport and bypasses middleware and `decodeBinary`. For decoded > output, call the named operation method such as > `client.connectors.salesModel.executeQuery(input)`. --- --- title: "@microsoft/rayfin-core" description: "Complete decorator reference for @microsoft/rayfin-core — entities, field types, relationships, and permissions that generate Data API Builder configuration." url: https://rayfin.ai/docs/reference/sdk/rayfin-core markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-core.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T01:28:43-07:00 source: reference/sdk/rayfin-core.mdx --- # @microsoft/rayfin-core > Complete decorator reference for @microsoft/rayfin-core — entities, field types, relationships, and permissions that generate Data API Builder configuration. `@microsoft/rayfin-core` provides the decorators you apply to TypeScript classes in `rayfin/data/` to define your data model. Decorators store metadata at runtime via `Symbol.metadata`; the Rayfin CLI reads that metadata to generate Data API Builder (DAB) configuration when you run `rayfin up` or `rayfin up db apply`. ## Installation [#installation] ```bash npm install @microsoft/rayfin-core ``` `npm create @microsoft/rayfin@latest` and `rayfin init` configure this for you automatically. If you are adding Rayfin to an existing project, configure both manually. ## TC39 Stage 3 decorators [#tc39-stage-3-decorators] Rayfin's decorators use the TC39 Stage 3 decorators proposal, not the older experimental decorators used by frameworks like NestJS or Angular. * Never enable `experimentalDecorators` or `emitDecoratorMetadata` in `tsconfig.json`. * Include `ESNext.Decorators` in the `lib` array. * No `reflect-metadata` import is required. Metadata is stored on the class itself via the native `Symbol.metadata` object, which the CLI and client SDKs read directly. ```json title="tsconfig.json" { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "ESNext.Decorators"], "module": "NodeNext", "moduleResolution": "NodeNext" } } ``` > [!WARNING] > Bundlers that compile decorators to an older target silently break Rayfin's metadata. > If you see `Expression expected` errors on `@entity()` or other decorators, your build > target is too old. For Vite, set `target: 'es2022'` and use `@vitejs/plugin-react` > (esbuild) — `@vitejs/plugin-react-swc` does not support TC39 Stage 3 decorators > regardless of the configured target. ## Class decorators [#class-decorators] ### `@entity()` [#entity] Marks a class as a DAB entity — required on every class you want exposed through the data API. Entity settings are inferred from the class name and convention: the entity name is the kebab-case class name, and the source table is its pluralized snake\_case form. ```typescript export declare function entity(name?: string): >( _target: T, _context: ClassDecoratorContext, ) => void; ``` ```typescript title="rayfin/data/Todo.ts" import { entity, uuid, text } from '@microsoft/rayfin-core'; @entity() export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; } ``` Every entity has an `id` field that serves as its UUID primary key. If you don't declare `id` explicitly, Rayfin adds it to the schema automatically; the field is always optional on create (the server generates one when omitted). Composite or non-`id` primary keys are not supported. ### `@blob()` [#blob] Marks a class as a blob storage folder configuration, used with `@microsoft/rayfin-storage`. Permissions and visibility (public vs. private) are inferred from the class's `@role()` / `@authenticated()` decorators — a folder is public if the `anonymous` role has any granted permissions. ```typescript export declare function blob(_folderName?: string): >( _target: T, context: ClassDecoratorContext, ) => void; ``` The parameter sets the folder name; it defaults to the kebab-case class name. ```typescript import { blob, role } from '@microsoft/rayfin-core'; @blob('uploads') @role('authenticated', '*') export class FileModel { owner_id!: string; } ``` ## Field decorators [#field-decorators] Every field on an `@entity()` class needs exactly one type decorator. All of them accept a shared set of base options, plus type-specific options. ### Shared base options [#shared-base-options] | Option | Type | Default | Description | | ---------- | ------------------------ | ------- | -------------------------------------------------------------------------------------------------- | | `optional` | `boolean` | `false` | Allows `NULL`. Fields are **required by default** — combine with a TypeScript `?` on the property. | | `unique` | `boolean` | `false` | Adds a unique constraint. | | `default` | matches the field's type | none | Default value for the column. | ### `@text(options?)` / `@email(options?)` [#textoptions--emailoptions] Variable-length text. `@email()` accepts the identical option type (`TextFieldOptions`) and adds an email-format hint used by downstream validation and UI — it does not have its own narrower option set. | Option | Type | Default | Description | | ------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `max` | `number` | dialect maximum | Maximum character length. Omitting it (or passing `-1`) falls back to the database's maximum, which on MSSQL is `NVARCHAR(MAX)` — this breaks GraphQL schema generation at deploy time. Always set `max` explicitly; see [Known limitations](/docs/reference/known-limitations). | | `min` | `number` | none | Minimum character length. | | `regex` | `RegExp` | none | Pattern the value must match. | ```typescript import { entity, uuid, text, email } from '@microsoft/rayfin-core'; @entity() export class Author { @uuid() id!: string; @text({ max: 200 }) name!: string; @text({ optional: true, max: 2000 }) bio?: string; @email({ unique: true }) emailAddress!: string; } ``` ### `@uuid(options?)` [#uuidoptions] UUID identifier — the conventional type for primary keys and foreign key columns. Accepts only the shared base options (no type-specific ones). A foreign key column that references another entity's `@uuid()` primary key must itself be `@uuid()` — a `@text()` field cannot stand in for it (an auth-derived field like `user_id` sourced from `claims.sub` is a `@text()` field, not a foreign key, so this rule doesn't apply to it). ```typescript @uuid() id!: string; @uuid() category_id!: string; // foreign key — must match the referenced PK type ``` ### `@int(options?)` [#intoptions] Whole numbers. | Option | Type | Default | Description | | ------------- | -------- | ------- | ------------- | | `max` / `min` | `number` | none | Value bounds. | ### `@decimal(options?)` [#decimaloptions] Fixed-point decimal, for money and other precise numeric values. | Option | Type | Default | Description | | ------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------- | | `max` / `min` | `number` | none | Value bounds. | | `precision` | `number` | `18` | Total digits. Maximum `28` (the Data API Builder runtime limit). Must be specified together with `scale`. | | `scale` | `number` | `2` | Digits after the decimal point. Must be between `0` and `precision`. | With neither option set, `@decimal()` defaults to `DECIMAL(18,2)`. ```typescript @decimal() price!: number; // DECIMAL(18,2) @decimal({ precision: 10, scale: 4 }) weight!: number; // DECIMAL(10,4) ``` ### `@boolean(options?)` [#booleanoptions] True/false values. Base options only. ### `@date(options?)` [#dateoptions] ISO-8601 date/time, serialized from `Date` objects or ISO strings. Base options only. ### `@set(...)` [#set] String enum with a database check constraint. `@set` has three call forms, all producing the same field behavior — pick whichever reads best at the call site: ```typescript type FieldDecoratorFn = ( target: unknown, context: ClassFieldDecoratorContext, ) => void; // 1. Values only function set(...values: T): FieldDecoratorFn; // 2. A single options object, with values in `enum` function set(options: SetFieldOptions): FieldDecoratorFn; // 3. Base options object, followed by values function set( options: Omit, 'enum'>, ...values: T ): FieldDecoratorFn; ``` `SetFieldOptions` adds one required property, `enum: T`, on top of the shared base options. ```typescript // Form 1 — values only @set('todo', 'in-progress', 'done') status!: 'todo' | 'in-progress' | 'done'; // Form 2 — options object with `enum` @set({ enum: ['todo', 'in-progress', 'done'], optional: true }) status?: 'todo' | 'in-progress' | 'done'; // Form 3 — base options, then values @set({ optional: true }, 'todo', 'in-progress', 'done') status?: 'todo' | 'in-progress' | 'done'; ``` ### Field type to database mapping [#field-type-to-database-mapping] | Decorator | Logical type | Database mapping | | ------------ | ------------ | -------------------------------------------- | | `@uuid()` | UUID | `UNIQUEIDENTIFIER` | | `@text()` | string | `NVARCHAR(n)`, sized from `max` | | `@int()` | integer | `INT` | | `@decimal()` | decimal | `DECIMAL` | | `@boolean()` | boolean | `BIT` | | `@date()` | datetime | `DATETIME2` | | `@email()` | string | Same as `@text()`, plus an email-format hint | | `@set()` | enum | String column with a check constraint | ## Relationship decorators [#relationship-decorators] `@one()` and `@many()` take a lazy `() => Target` function (not the class directly) so entities in different files can reference each other without import-order issues. Both accept only `RelationshipFieldOptions` — the base options minus `default`: | Option | Type | Default | Description | | ---------- | --------- | ------- | ---------------------------------------------------------------- | | `optional` | `boolean` | `false` | Nullable relationship (optional foreign key). | | `unique` | `boolean` | `false` | Makes a `@one()` relationship one-to-one instead of many-to-one. | Custom foreign key or target key names are not supported — see [Known limitations](/docs/reference/known-limitations). ### `@one(target, options?)` [#onetarget-options] Many-to-one (or one-to-one with `unique: true`). Rayfin auto-generates a foreign key column named `{property}_id` — you don't need to declare it unless your application code reads or writes the raw ID. ```typescript import { entity, uuid, text, one } from '@microsoft/rayfin-core'; import { Category } from './Category.js'; // `import`, not `import type` — the decorator needs the runtime class @entity() export class Todo { @uuid() id!: string; @text() title!: string; @one(() => Category) category!: Category; // generates category_id @one(() => Category, { optional: true }) parentCategory?: Category; } ``` ### `@many(target, options?)` [#manytarget-options] The inverse, one-to-many side of a `@one()` relationship. ```typescript import { entity, uuid, text, many } from '@microsoft/rayfin-core'; import { Todo } from './Todo.js'; @entity() export class Category { @uuid() id!: string; @text() name!: string; @many(() => Todo) todos?: Todo[]; } ``` Many-to-many relationships are not supported directly — model them with an explicit join entity that has two `@one()` fields. See [Known limitations](/docs/reference/known-limitations). > [!NOTE] > `@microsoft/rayfin-core` ships a built-in, read-only `User` system entity (`id`, `email`) > that is always a valid `@one()` / `@many()` target — you don't need to declare it in your > own schema to reference it. ## Permission decorators [#permission-decorators] Every entity needs an explicit permission decorator. Omitting one is not the same as denying access — it silently applies `authenticated: *` (full CRUD for any signed-in user), which is rarely what you want in production. ### `@authenticated(actions?, options?)` [#authenticatedactions-options] Shorthand for the built-in `authenticated` role — requires a valid user session. ```typescript type RoleDecoratorFn = ( target: constructor, context: ClassDecoratorContext>, ) => void; function authenticated( actions?: SimpleAction | SimpleAction[], // default: '*' options?: RoleDeclarationOptions, ): RoleDecoratorFn; ``` | Parameter | Type | Default | Description | | ----------------- | ------------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------- | | `actions` | `'create' \| 'read' \| 'update' \| 'delete' \| 'execute' \| '*'`, or an array of them | `'*'` | The DAB action(s) this role may perform. `'execute'` applies to stored procedures. | | `options.policy` | `(claims, item) => PolicyExpression` | none | Row-level filter — see [Policy DSL](#policy-dsl-and-field-visibility) below. | | `options.include` | `(keyof TEntity)[]` | none | Restrict the role to only these fields. | | `options.exclude` | `(keyof TEntity)[]` | none | Hide these fields from the role. | ```typescript title="rayfin/data/Todo.ts" import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @authenticated('*', { policy: (claims, item) => claims.sub.eq(item.user_id), }) export class Todo { @uuid() id!: string; @text({ max: 200 }) title!: string; @text() user_id!: string; } ``` ### `@role(roleName, actions, options?)` [#rolerolename-actions-options] The general-purpose form that `@authenticated()` and `@anonymous()` are shorthands for. ```typescript function role( roleName: 'authenticated' | 'anonymous', actions: SimpleAction | SimpleAction[], options?: RoleDeclarationOptions, ): RoleDecoratorFn; ``` ### `@anonymous(actions?, options?)` [#anonymousactions-options] Public, unauthenticated access. Exported from the package root: ```typescript function anonymous( actions?: SimpleAction | SimpleAction[], // default: '*' options?: RoleDeclarationOptions, ): RoleDecoratorFn; ``` ```typescript import { entity, uuid, text, anonymous, authenticated } from '@microsoft/rayfin-core'; @entity() @anonymous('read') @authenticated('*') export class PublicPost { @uuid() id!: string; @text({ max: 200 }) title!: string; } ``` > [!NOTE] > Anonymous access on a deployed Fabric app also requires a tenant administrator to enable > anonymous data access for the tenant. See > [Permissions](/docs/data/permissions) for the full picture. > [!WARNING] > Anonymous data access is not currently supported when deployed to Fabric: the CLI rejects > any DAB configuration that grants the `anonymous` role at apply time, regardless of > whether you used `@anonymous(...)` or `@role('anonymous', ...)`. Treat this as a preview > of a future stable API, and prefer `@anonymous()` (rather than the `role()` widening) so > the intent is clear at the call site. To use anonymous data with Microsoft Fabric, > contact your tenant admin about the "Enable anonymous data access for Fabric Apps" setting. ### Policy DSL and field visibility [#policy-dsl-and-field-visibility] The `policy` callback receives two typed arguments and returns a composable expression that compiles to a DAB policy string: * `claims` — a `ClaimsDsl` exposing `claims.sub`, `claims.email`, and `claims.role`. These are the only supported claims. * `item` — a proxy with one property per entity field, typed from the class itself so a typo fails at compile time. Both `claims.` and `item.` support `.eq(value)` and `.neq(value)`, each returning a `PolicyExpression`. Combine expressions with `.and()` / `.or()`, which parenthesize both sides automatically: ```typescript policy: (claims, item) => claims.role.eq('admin').or(claims.sub.eq(item.owner_id).and(item.isActive.eq(true))) ``` Use `include` / `exclude` on any role decorator to control field-level visibility per action. Both arrays are typed against the entity's actual property names: ```typescript import { entity, authenticated, uuid, text } from '@microsoft/rayfin-core'; @entity() @authenticated('read', { exclude: ['secret'] }) @authenticated(['create', 'update'], { policy: (claims, item) => claims.sub.eq(item.owner_id), }) export class Document { @uuid() id!: string; @text() owner_id!: string; @text() title!: string; @text({ optional: true }) secret?: string; } ``` Multiple role decorators on the same class are aggregated; conflicting declarations for the same role and action produce a warning at generation time. ## Form validation [#form-validation] `@microsoft/rayfin-core` can build a [Standard Schema](https://standardschema.dev) validator directly from an `@entity()` class, so you can validate form input without a separate library. ```typescript function toStandardSchema( entity: EntityClass, options?: { omit?: readonly K[] }, ): RayfinStandardSchema>; function getFieldConstraints( entity: EntityClass, field: K & string, ): FieldConstraints | undefined; ``` `toStandardSchema` automatically omits the `id` primary key and any `@one()` / `@many()` relationship properties — pass additional field names (like timestamps) via `omit`. ```typescript import { toStandardSchema, getFieldConstraints } from '@microsoft/rayfin-core'; import { Todo } from '../rayfin/data/Todo.js'; const todoInputSchema = toStandardSchema(Todo, { omit: ['createdAt', 'updatedAt'] as const, }); const result = todoInputSchema.validate({ title: 'Write docs' }); if (result.issues) { // result.issues: readonly StandardSchemaV1.Issue[] } else { // result.value is typed as Omit } const titleConstraints = getFieldConstraints(Todo, 'title'); // { type: 'string', min: 1, max: 200, optional: false } ``` The returned object implements the `StandardSchemaV1` contract (`~standard`), so it works directly with TanStack Form, Conform, tRPC v11, and any other Standard Schema–compatible library, in addition to its own convenience `.validate()` method. --- --- title: "@microsoft/rayfin-data" description: "The fluent GraphQL query and mutation API behind client.data — select, where, orderBy, pagination, and CRUD methods with exact signatures." url: https://rayfin.ai/docs/reference/sdk/rayfin-data markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-data.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-29T23:37:34-07:00 source: reference/sdk/rayfin-data.mdx --- # @microsoft/rayfin-data > The fluent GraphQL query and mutation API behind client.data — select, where, orderBy, pagination, and CRUD methods with exact signatures. `@microsoft/rayfin-data` is the DAB-compliant data client that powers `client.data` on both [`RayfinClient` and `RayfinServerClient`](/docs/reference/sdk/rayfin-client). Most applications never import it directly — you get a typed instance for free from `@microsoft/rayfin-client` — but it is a standalone package if you want the query builder without the rest of the SDK. ## Installation [#installation] ```bash npm install @microsoft/rayfin-data ``` ## Getting a client [#getting-a-client] `createDataApi` builds the typed proxy that `client.data` is: ```typescript function createDataApi( apiClient: ApiClient, ): DataApi & TypedDataClients; type TypedDataClients = { [K in keyof TSchema]: GraphQLEntityClient; }; ``` Each property on the result — `dataApi.Todo`, `dataApi.Note`, and so on — is a `GraphQLEntityClient` scoped to that entity. ## Query chain [#query-chain] `GraphQLEntityClient` exposes both direct query methods and a fluent builder (`GraphQLQueryBuilder`) for composing `select` / `where` / `orderBy` / pagination: ```typescript class GraphQLEntityClient { select>(fields: TFields): GraphQLQueryBuilder; where(conditions: FilterInput): GraphQLQueryBuilder; orderBy(order: OrderByInput): GraphQLQueryBuilder; first(count: number): GraphQLQueryBuilder; findMany(filter?: FilterInput): Promise; findFirst(filter?: FilterInput): Promise; findById(id: string): Promise; create(input: CreateInput): Promise; update(where: WhereUniqueInput, data: UpdateInput): Promise; delete(where: WhereUniqueInput): Promise; upsert( where: WhereUniqueInput, create: CreateInput, update: UpdateInput, ): Promise; } class GraphQLQueryBuilder { select>(fields: TFields): this; where(conditions: FilterInput): this; orderBy(order: OrderByInput): this; first(count: number): this; after(cursor: string): this; execute(): Promise; executePaginated(): Promise>; findFirst(): Promise; } ``` `select`, `where`, `orderBy`, and `first` return `this`, so they chain in any order before a terminal call to `execute()`, `executePaginated()`, or `findFirst()`. ### Reading records [#reading-records] ```typescript const notes = await client.data.Note.select([ 'id', 'title', 'isPinned', 'notebook.id', // dot-path — only valid inside select(), not where() 'notebook.name', ]) .where({ isPinned: { eq: true } }) .orderBy({ createdAt: 'desc' }) .execute(); ``` `.execute()` returns a single page — the Data API caps a response at its default page size (100 records) even when the underlying table has more rows, and gives no signal that more records exist. Use it only for queries you know are bounded (a `.where()` filter that can match at most a handful of rows, or a small lookup table); for anything that can grow unbounded, use pagination instead. ### Filtering — `FilterInput` [#filtering--filterinput] `where()` takes one entry per field, keyed to a type-specific filter shape, plus optional `and` / `or` arrays for boolean composition: ```typescript type FilterInput = { [K in keyof T]?: FilterValue; } & { and?: FilterInput[]; or?: FilterInput[]; }; ``` | Field type | Filter operators | | -------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `string` (`StringFilterInput`) | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `notContains`, `startsWith`, `endsWith`, `isNull`, `in` | | `number` (`NumberFilterInput`) | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `isNull`, `in` | | `boolean` (`BooleanFilterInput`) | `eq`, `neq`, `isNull`, `in` | | `Date` (`DateFilterInput`) | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `isNull`, `in` | | relationship field | `{ isNull: boolean }` (only when the relationship itself is optional) | Filter by the foreign key column (`customer_id`), not a relationship dot-path (`customer.id`) — dot-paths are select-only. See [Known limitations](/docs/reference/known-limitations). ### Sorting — `OrderByInput` [#sorting--orderbyinput] ```typescript type OrderByInput = { [K in keyof T]?: 'asc' | 'desc' }; ``` Directions are lowercase strings, not an enum. ### Pagination [#pagination] ```typescript interface PaginationConfig { first?: number; after?: string; } interface PagedResult { items: T[]; hasNextPage: boolean; endCursor?: string; totalCount?: number; } ``` Data API Builder only supports forward pagination (`first` / `after`) — there is no `before` / `last`. Use `.first(n)` to set the page size and `.executePaginated()` to get a page plus cursor metadata; pass the previous page's `endCursor` to `.after()` for the next page: ```typescript const page = await client.data.Note.select(['id', 'title']) .orderBy({ createdAt: 'desc' }) .first(25) .executePaginated(); // page.items, page.hasNextPage, page.endCursor const nextPage = await client.data.Note.select(['id', 'title']) .orderBy({ createdAt: 'desc' }) .first(25) .after(page.endCursor!) .executePaginated(); ``` Keep `select`, `where`, and `orderBy` identical across pages — a stable sort order is required for the cursor to advance correctly. `first(n)` is bounded by DAB's maximum page size (100,000); `totalCount` is present on `PagedResult` but is not populated by DAB today. There is no `count()` method on the query chain. `count` exists as an [aggregation](/docs/data/aggregations) operation over numeric fields — see [Aggregation](#aggregation) below. For a row count, select minimal fields and use `results.length`, or `page.items.length` per page. See [Known limitations](/docs/reference/known-limitations). ## Aggregation [#aggregation] `groupBy()` and `aggregate()` are entry points on both `GraphQLEntityClient` and `GraphQLQueryBuilder`. They return a `GraphQLAggregationBuilder`, whose `execute()` resolves to one row per group. Calling `aggregate()` without `groupBy()` produces a single grand-total row whose `fields` is empty. ```typescript class GraphQLEntityClient { groupBy[]>( fields: TGroup, ): GroupedAggregationStage; aggregate>( spec: TSpec, ): GraphQLAggregationBuilder; } class GraphQLAggregationBuilder { execute(): Promise[]>; } ``` The specification is keyed by aliases you choose. Each entry holds **exactly one** operation, whose value is either a field-name shorthand or an options object: ```typescript type AggregationOps = { sum: AggregationOpValue>; avg: AggregationOpValue>; min: AggregationOpValue>; max: AggregationOpValue>; count: AggregationOpValue>; }; type AggregationOpValue = F | { field: F; having?: NumberFilterInput; distinct?: boolean }; type AggregationSpec = Record>>; interface GroupedAggregationRow[], S> { fields: Pick; aggregations: { [K in Extract]: AggregationResult }; } ``` `AggregationResult` is `number` for `count` and `number | null` for `sum` / `avg` / `min` / `max`, because DAB emits those as nullable and SQL returns `NULL` over an empty or all-null group. ```typescript const rows = await client.data.Order .where({ status: { eq: 'shipped' } }) .groupBy(['region']) .aggregate({ revenue: { sum: 'amount' }, biggest: { max: { field: 'amount', having: { gt: 500 } } }, }) .execute(); ``` > [!NOTE] > Every operation — `count` included — is typed against `NumericKeys`, because DAB > generates each aggregation's `field` argument as the entity's `NumericAggregateFields` > enum. Non-numeric fields are a compile error. Aliases and field tokens are validated at runtime against the GraphQL name grammar (`/^[_A-Za-z][_0-9A-Za-z]*$/`, and must not begin with `__`). DAB rejects a query combining `groupBy` with `items`, so `aggregate()` throws if it follows `select()`, `first()`, `after()`, or `orderBy()`; the `RowQueryBuilder` return type makes those combinations compile-time errors as well. ## Mutations [#mutations] ```typescript // MutationInput turns @one() relationship fields into "full object or { id }" inputs type CreateInput = Omit, 'id'> & Partial>; type UpdateInput = Partial>; type WhereUniqueInput = { id: string }; ``` `create`, `update`, `delete`, and `upsert` are available both directly on `GraphQLEntityClient` (`client.data.Todo.create(...)`) and do not go through the query builder. ```typescript const todo = await client.data.Todo.create({ title: 'Write docs', isCompleted: false, user_id: session.user.id, }); await client.data.Todo.update({ id: todo.id }, { isCompleted: true }); await client.data.Todo.delete({ id: todo.id }); await client.data.Todo.upsert( { id: todo.id }, { title: 'Write docs', isCompleted: false, user_id: session.user.id }, { isCompleted: true }, ); ``` ### Relationship fields in mutations [#relationship-fields-in-mutations] A `@one()` relationship field in `create` / `update` input accepts either the full related object or an object with just the primary key — both produce the same GraphQL mutation. The primary-key-only form is the recommended shorthand: ```typescript // ID-only shorthand (recommended) await client.data.Note.create({ title: 'Meeting notes', notebook: { id: notebookId }, }); // Full object also works await client.data.Note.create({ title: 'Meeting notes', notebook: notebookObject, }); ``` `@many()` array fields are accepted on mutation input but ignored at runtime — manage the inverse side by updating the child entity's `@one()` field instead. ## Field selection and types [#field-selection-and-types] ```typescript type FieldSelection = readonly (CleanEntityKeys | NestedFieldPath)[]; ``` `select()` accepts entity field names and one level of relationship dot-paths (`'notebook.name'`). Nested queries beyond the second level are not supported — see [Known limitations](/docs/reference/known-limitations). ## Advanced: the underlying GraphQL client [#advanced-the-underlying-graphql-client] `@microsoft/rayfin-data` also exports the lower-level pieces `GraphQLEntityClient` builds on, for advanced or standalone use: ```typescript class GraphQLClient { constructor(apiClient: ApiClient, endpoint?: string); request(query: string, variables?: Record, operationName?: string): Promise; query(query: string, variables?: Record): Promise; mutation(mutation: string, variables?: Record): Promise; } ``` Most applications should use `client.data.` rather than calling `GraphQLClient` directly — it exists so `GraphQLEntityClient` and `GraphQLQueryBuilder` have a raw query/mutation execution primitive to build on. --- --- title: "@microsoft/rayfin-functions" description: "FunctionClient, the FunctionsSchema type, and client.functions..invoke() for calling serverless functions from the Rayfin SDK." url: https://rayfin.ai/docs/reference/sdk/rayfin-functions markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-functions.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: reference/sdk/rayfin-functions.mdx --- # @microsoft/rayfin-functions > FunctionClient, the FunctionsSchema type, and client.functions..invoke() for calling serverless functions from the Rayfin SDK. `@microsoft/rayfin-functions` provides the typed client behind `client.functions..invoke()` on [`RayfinClient`](/docs/reference/sdk/rayfin-client). It depends only on [`@microsoft/rayfin-lib`](/docs/reference/sdk/rayfin-lib). > [!WARNING] > This package is marked **experimental** by its own README and may change substantially. > The functions service is not available in every Fabric region or tenant. The exact return > shape of `invoke()` has changed across versions (see > [Version notes](#version-notes) below) — confirm the behavior for your installed version > with `rayfin docs get --symbol FunctionClient` or the [MCP server](/docs/reference/cli/docs#mcp-server) > before relying on it in production code. ## Installation [#installation] ```bash npm create @microsoft/rayfin@latest ``` `@microsoft/rayfin-functions` is normally installed as a dependency of `@microsoft/rayfin-client`; you rarely add it directly. ## Declaring a `FunctionsSchema` [#declaring-a-functionsschema] Define a type that maps each function name to its input and output, and pass it as `RayfinClient`'s third type parameter so `client.functions..invoke()` is fully typed: ```typescript title="rayfin/functions/src/types.ts" import type { FunctionsSchema } from '@microsoft/rayfin-functions'; export type MyFunctionsSchema = { helloWorld: { input: { firstName: string; lastName: string }; output: string }; add: { input: { a: number; b: number }; output: number }; noParams: { input: void; output: string }; // use `void` (or `{}`) for no-input functions } satisfies FunctionsSchema; ``` ```typescript type FunctionsSchema = Record; ``` ```typescript import { RayfinClient } from '@microsoft/rayfin-client'; import type { MyFunctionsSchema } from '../rayfin/functions/src/types'; import type { AppSchema } from '../rayfin/data/schema'; const client = new RayfinClient({ baseUrl: 'https://-app.rayfin.windows.net/', publishableKey: 'pk-commonSampleAppKey', }); ``` ## Invoking a function [#invoking-a-function] ```typescript const greeting = await client.functions.helloWorld.invoke({ firstName: 'Ada', lastName: 'Lovelace', }); ``` `client.functions` is built by `createFunctionsApi`, which lazily instantiates and caches one `FunctionClient` per schema entry: ```typescript function createFunctionsApi( apiClient: ApiClient, ): TypedFunctionClients; type TypedFunctionClients = { [K in keyof TSchema & string]: FunctionClient; }; ``` ## `FunctionClient` [#functionclient] ```typescript class FunctionClient { constructor(apiClient: ApiClient, functionName: string); invoke( ...args: TInput extends void | Record ? [options?: InvokeOptions] : [params: TInput, options?: InvokeOptions] ): Promise; } interface InvokeOptions { headers?: Record; } ``` * When the schema's `input` is `void`, call `invoke(options?)` with no params. * Otherwise call `invoke(params, options?)`. `options.headers` adds extra headers to that one request. * `invoke()` resolves to the function's output directly, typed as `TOutput`. If the raw response contains a JSON-encoded string, it is auto-parsed so the caller never has to. * Failures throw rather than returning an error value: a non-empty `errors` array or non-success status is surfaced as `FunctionsError`; network failures as `NetworkError`; anything else as `SdkError`. ```typescript import { FunctionsError } from '@microsoft/rayfin-functions'; import { NetworkError } from '@microsoft/rayfin-lib'; try { const result = await client.functions.add.invoke({ a: 1, b: 2 }); console.log(result); // 3, typed as number } catch (error) { if (error instanceof FunctionsError) { console.error('Function failed:', error.message, error.code); } else if (error instanceof NetworkError) { console.error('Network issue:', error.message); } } ``` ## `FunctionsError` [#functionserror] ```typescript class FunctionsError extends SdkError { constructor(message: string, code?: string); } ``` ## Full exported surface (`index.d.ts`) [#full-exported-surface-indexdts] ```typescript export { FunctionsError, createFunctionsApi, FunctionClient, } from './Functions.js'; export type { FunctionInvocationResponse, InvokeOptions, TypedFunctionClients, } from './Functions.js'; export type { FunctionsSchema } from './FunctionsSchema.js'; ``` `FunctionInvocationResponse` is the raw wire envelope the function endpoint returns before `invoke()` unwraps it: ```typescript interface FunctionInvocationResponse { functionName: string; invocationId: string; status: string; output: TOutput; errors: Array>; } ``` ## Version notes [#version-notes] The exact shape `invoke()` resolves to has changed between releases of this experimental package: * **Documented / current behavior** (shown above): `invoke()` resolves directly to `TOutput` — the envelope's `output` field, auto-unwrapped — and throws on failure instead of returning an `errors` array. The `invocationId` is still emitted via `console.debug` for correlation, without being part of the typed return value. * **This machine's installed version (1.31.0)**: `invoke()` instead resolves to the full `Promise>` envelope — callers must read `.output` themselves and check `.errors` / `.status` manually. Check which behavior applies to your project before writing calling code — the two shapes are not interchangeable (`result.output` vs. `result` directly). --- --- title: "@microsoft/rayfin-lib" description: "The shared ApiClient, error classes, and small utilities every other Rayfin SDK package builds on — an internal dependency most builders never import directly." url: https://rayfin.ai/docs/reference/sdk/rayfin-lib markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-lib.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-22T22:02:07-07:00 source: reference/sdk/rayfin-lib.mdx --- # @microsoft/rayfin-lib > The shared ApiClient, error classes, and small utilities every other Rayfin SDK package builds on — an internal dependency most builders never import directly. `@microsoft/rayfin-lib` is the shared HTTP client and utility layer underneath every other `@microsoft/rayfin-*` SDK package — [`rayfin-core`](/docs/reference/sdk/rayfin-core), [`rayfin-data`](/docs/reference/sdk/rayfin-data), [`rayfin-auth`](/docs/reference/sdk/rayfin-auth), [`rayfin-auth-provider-fabric`](/docs/reference/sdk/rayfin-auth-provider-fabric), and [`rayfin-functions`](/docs/reference/sdk/rayfin-functions) all depend on it. Most Rayfin applications never import it directly — you get its exports re-exported through `@microsoft/rayfin-client` (`ApiClientConfig`, the error classes) — but it's useful to know what lives here when you're reading a stack trace or building against the SDK at a lower level. ## Installation [#installation] ```bash npm install @microsoft/rayfin-lib ``` ## `ApiClient` [#apiclient] The isomorphic HTTP client every higher-level SDK class wraps. It works in both browsers and Node.js using native `fetch`, with automatic `401` retry when a refresh callback is configured. ```typescript class ApiClient { constructor(config: ApiClientConfig); setAccessTokenCallback(callback: () => string | null): void; setRefreshCallback(callback: () => Promise): void; get(path: string, options?: RequestOptions): Promise; post(path: string, data?: any, options?: RequestOptions): Promise; put(path: string, data?: any, options?: RequestOptions): Promise; delete(path: string, options?: RequestOptions): Promise; requestRaw(path: string, options?: RequestRawOptions): Promise; } interface ApiClientConfig { baseUrl: string; publishableKey: string; headers?: Record; timeout?: number; getAccessToken?: () => string | null; useProxy?: boolean; onRefreshNeeded?: () => Promise; } ``` See [`@microsoft/rayfin-client`](/docs/reference/sdk/rayfin-client#rayfinclientconfig) for the option table — `RayfinClientConfig` and `RayfinServerClientConfig` both extend `ApiClientConfig`. `setAccessTokenCallback` and `setRefreshCallback` are how `Auth` attaches itself to an `ApiClient` after construction (`Auth.attachToClient()`), which is also how `@microsoft/rayfin-auth-provider-fabric` gets its token-refresh behavior for free. ## Errors [#errors] Every SDK package's thrown errors extend this hierarchy: ```typescript class SdkError extends Error { name: string; code?: string; constructor(message: string, code?: string); } class AuthError extends SdkError {} class NetworkError extends SdkError { status?: number; constructor(message: string, status?: number, code?: string); } ``` `@microsoft/rayfin-client` declares its **own** `AuthError extends SdkError` rather than re-exporting this one — the two classes are structurally identical but distinct; an `instanceof` check needs the `AuthError` from whichever package actually threw it. See [Errors](/docs/reference/sdk/rayfin-client#errors) on the client reference page. ## Small utilities [#small-utilities] Two narrowly-scoped helpers are exported from the package root, used internally by the data client and storage tooling: ```typescript function normalizeContainerName(input: string): string; class EntityNameResolver { static getPlural(entityName: string): string; static getSingular(pluralName: string): string; static isPlural(word: string): boolean; static setCustomPlural(singular: string, plural: string): void; static clearCustomPlurals(): void; } ``` `normalizeContainerName` lowercases and hyphenates a string to satisfy Azure Blob container naming rules (letters, numbers, and hyphens only; 3–63 characters). `EntityNameResolver` backs the automatic singular/plural table-name inference `@microsoft/rayfin-core` performs for `@entity()` classes (a `Todo` class maps to a `todos` table), and lets you register irregular plurals it wouldn't guess correctly on its own. ## What's intentionally not documented here [#whats-intentionally-not-documented-here] `ServicePlugin` (an abstract base class for building custom service plugins) is exported from the package but marked `@alpha` and `@hidden` in its own source comments — it is not considered part of the public API yet, so this page does not document its shape. --- --- title: "@microsoft/rayfin-storage" description: "Type-safe blob storage client for Rayfin — what it's for and how to model storage folders today, pending a version-locked API reference." url: https://rayfin.ai/docs/reference/sdk/rayfin-storage markdown_url: https://rayfin.ai/docs/reference/sdk/rayfin-storage.md section: reference product: Rayfin sdk_version: 1.34.0 cli_version: 1.33.2 last_updated: 2026-08-23T15:47:11-07:00 source: reference/sdk/rayfin-storage.mdx --- # @microsoft/rayfin-storage > Type-safe blob storage client for Rayfin — what it's for and how to model storage folders today, pending a version-locked API reference. `@microsoft/rayfin-storage` is Rayfin's type-safe blob storage client. Per the package catalog, it provides a storage client for Rayfin storage backends (blob upload, download, and file management). > [!WARNING] > Storage is experimental and is not available in every Fabric region or tenant. > `@microsoft/rayfin-storage` may change substantially between releases. > [!NOTE] > `@microsoft/rayfin-storage` is not installed in the environment this reference was > written against, so its method signatures cannot be verified here. Run > `rayfin docs search 'storage' --module ts-sdk` (or the MCP server's > `search_docs(query: 'storage', module: 'ts-sdk')`) from your project root for the > version-locked API surface of the package you actually have installed. See > [MCP server](/docs/reference/cli/docs#mcp-server) for how those lookups work. ## Installation [#installation] ```bash npm install @microsoft/rayfin-storage ``` Storage is also gated behind a CLI feature flag in current builds — set `RAYFIN_FEATURE_FLAGS=storage` to expose storage prompts in `rayfin init`. ## What is verifiable today [#what-is-verifiable-today] The one part of the storage model that is verified independently — because it lives in `@microsoft/rayfin-core`, which *is* installed — is the `@blob()` class decorator you use to declare a storage folder: ```typescript import { blob, role } from '@microsoft/rayfin-core'; @blob('uploads') @role('authenticated', '*') export class FileModel { owner_id!: string; } ``` * `@blob(folderName?)` marks a class as a blob storage folder. The name defaults to the kebab-case class name when omitted. * Permissions use the same `@role()` / `@authenticated()` / `@anonymous()` decorators as data entities (see [`@microsoft/rayfin-core`](/docs/reference/sdk/rayfin-core)) — a folder is inferred public if the `anonymous` role has any granted permission on it, otherwise private. `@blob()` is what the Rayfin CLI reads to generate storage configuration when you run `rayfin up`. It does not, by itself, give you a client to upload or download files — that surface belongs to `@microsoft/rayfin-storage` and is out of scope for this page. ## What this page deliberately omits [#what-this-page-deliberately-omits] No client construction signature, upload/download method names, or options are documented here, because none could be confirmed against either the vendored package docs or an installed copy of the package. Documenting a guessed method signature would be worse than leaving it out — use the `rayfin docs` lookup above to get the real surface for your project's installed version.