---
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) => (
{column.name}
))}
{table.rows.map((row, rowIndex) => (
{table.columns.map((column, columnIndex) => (
{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 (
);
}
```
## 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 (
);
}
```
## 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 (
);
}
```
> [!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