rayfin-functions

$npx mdskill add microsoft/aitour27-BRK390-building-ai-applications-with-microsoft-databases-and-fabric/rayfin-functions

Write server-side user-defined functions (UDFs) that run in the Fabric runtime and are invocable from your frontend via `RayfinClient`.

SKILL.md

.github/skills/rayfin-functionsView on GitHub ↗
---
name: rayfin-functions
description: "Use when writing Rayfin user-defined functions (UDFs), invoking functions from frontend code, or using functions CLI commands. Triggers: rayfin functions, udf.func, UserDataFunctions, RayfinContext, FabricContext, AppFunctionsSchema, functions init, dev functions apply, client.functions, invoke, func start, function_app.ts, types.ts, rayfin/functions/, FunctionClient, FunctionsSchema, fabric-user-data-functions, functionsBaseUrl, VITE_RAYFIN_FUNCTIONS_URL, bootstrap.ts, rayfinClient.ts"
metadata:
  author: microsoft
  version: 0.1.0
rayfin-managed: true
---
# Rayfin Functions

Write server-side user-defined functions (UDFs) that run in the Fabric runtime and are invocable from your frontend via `RayfinClient`.

## When to Use Functions

Reach for functions whenever logic should run on the backend rather than the frontend. Use them for sensitive operations (secrets, API keys, privileged data access), business logic that must not be exposed or tampered with in client code, server-side validation, and anything that aggregates or transforms data before returning it to the client. If a frontend feature needs trusted server-side behavior, implement it as a function.

## Project Structure

After `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
    tsconfig.json       ← references: [{ "path": ".." }]
    package.json
    host.json
    local.settings.json
```

`tsconfig.json` uses `composite: true` with a project reference to `rayfin/` so functions can `import type` from data entities without duplicating definitions.

## Writing Functions

### Simple function (no context)

```ts
import { UserDataFunctions } from '@microsoft/fabric-user-data-functions';

const udf = new UserDataFunctions();

udf.func('greet', (firstName: string, lastName: string): string => {
  return `Hello ${firstName} ${lastName}!`;
}, []);
```

- First argument: function name (must match the key in `AppFunctionsSchema`).
- Second argument: handler function with typed parameters and return type.
- Third argument: empty array `[]` (reserved for future middleware).
- Parameters and return type are extracted by typegen into `types.ts`.

### Function with RayfinContext

Use `RayfinContext` to access the GraphQL-backed data client, auth tokens, and logging:

```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<AppSchema>): Promise<{ id: string; message: string }[]> => {
  console.log('getEntries invoked');
  const data = ctx.getDataClient();
  const entries = await data.Entry.select(['id', 'message', 'createdAt']).execute();
  return entries;
}, []);
```

**`RayfinContext` API:**
- `ctx.getDataClient()` — returns an entity data client (type-safe when `RayfinContext<AppSchema>` is used). Query with `.select([...]).where(...).execute()`, same chain as `client.data.<Entity>` in frontend code.
- Use `console.log(...)` / `console.error(...)` for logging (not `ctx.log`).
- `ctx.baseUrl` — Rayfin endpoint URL (readonly).
- `ctx.accessToken` — auth token for the current request (readonly).
- `ctx.publishableKey` — Rayfin publishable key (readonly).
- `ctx.getSecret(name)` — returns a secret from the host-provided secret bag first, and falls back to `process.env[name]` when the secret is not present in the bag.

**Type-safe context with `RayfinContext<AppSchema>`:**

Pass your `AppSchema` type (the same schema used with `RayfinClient<AppSchema>`) as a generic to `RayfinContext` for full type safety on `ctx.getDataClient()`:

- `ctx.getDataClient()` returns typed entity accessors — e.g. `data.Todo.select(['id', 'title'])` only allows valid fields.
- Without a generic (`RayfinContext` alone), `getDataClient()` still works but entity access is untyped (`Record<string, any>`).

- Import `RayfinContext` from `@microsoft/fabric-user-data-functions` (not from `@microsoft/rayfin-functions`).
- `RayfinContext` (with or without a generic) is a runtime-injected parameter — typegen automatically excludes it from the generated schema input type.
- Use `.js` extension on relative imports (ESM resolution).

### Function with mixed parameters

```ts
udf.func('addEntry', async (message: string, ctx: RayfinContext<AppSchema>): Promise<void> => {
  console.log('addEntry invoked');
  const data = ctx.getDataClient();
  await data.Entry.create({ message });
}, []);
```

Runtime-injected types (`RayfinContext`, `FabricContext`, `FabricItem`, `DataConnection`, `FabricSqlConnection`, `FabricConnection`, `FabricUdf`, `FabricLogger`) are stripped from the schema — only `message: string` appears in the generated `input` type.

### Function secrets via RayfinContext

Use `ctx.getSecret(name)` when the function needs a secret value.
The runtime checks host-provided invocation secrets first.
If a secret is missing there, it checks `process.env[name]` as a fallback.
Set project secrets with `rayfin secret set <NAME>`.
For local function debugging, add the same key in `rayfin/functions/local.settings.json` under `Values` so it flows into `process.env` and is available to `ctx.getSecret(name)`.

```ts
udf.func('readApiKey', async (ctx: RayfinContext): Promise<string> => {
  const apiKey = ctx.getSecret('THIRD_PARTY_API_KEY');
  if (!apiKey) {
    throw new Error('Missing THIRD_PARTY_API_KEY secret.');
  }
  return apiKey;
}, []);
```

## Using Connections for External Resources

Functions access external resources via **delegated authentication**. The runtime exchanges the user's identity token for a resource-scoped OBO token — your function accesses resources **as the calling user**. You declare a connection, and the runtime provides the scoped token at invocation time.

### Pattern

```ts
import { AudienceType } from '@microsoft/fabric-user-data-functions';

udf.func(
  'myFunction',
  async (ctx: RayfinContext, /* user params */): Promise<T> => {
    const token = ctx.getToken(AudienceType.X);
    // Use token with the appropriate SDK client
  },
  [udf.connection({ audienceType: AudienceType.X })],
);
```

For Azure SDK clients that expect a `TokenCredential`, wrap the token:

```ts
import type { TokenCredential, AccessToken } from '@azure/identity';

class ContextTokenCredential implements TokenCredential {
  constructor(private readonly token: string) {}
  async getToken(): Promise<AccessToken> {
    return { token: this.token, expiresOnTimestamp: Date.now() + 3600_000 };
  }
}
```

### Available `AudienceType` values

| AudienceType | Resource | Ask the user for |
|---|---|---|
| `Storage` | OneLake (DFS), Azure Blob/Table/Queue | File URL from Lakehouse properties, or storage account URL |
| `Sql` | Fabric Lakehouse/Warehouse/SQL DB/Mirrored DB, Azure SQL | SQL analytics endpoint or connection string from Fabric Portal |
| `KeyVault` | Azure Key Vault | Vault URL (e.g. `https://my-vault.vault.azure.net/`) |
| `CosmosDB` | Azure Cosmos DB | Account endpoint + database/container names |
| `EventGrid` | Azure Event Grid | Topic endpoint |
| `Kusto` | Azure Data Explorer (Kusto) | Cluster query URL (e.g. `https://<cluster>.<region>.kusto.windows.net`) |
| `PowerBI` | Power BI APIs | _(scopes TBD — check with platform team)_ |

### SQL connections: shared requirements

All Fabric SQL resources (Lakehouse SQL analytics, Warehouse, SQL Database, Mirrored DB) use the same pattern:

- **Package:** `mssql@^12.6.0` (pulls tedious >= 19.2.2). Older tedious (<= 19.1.2) has a LOGIN7 FeatureExt bug causing "socket hang up" on Fabric endpoints.
- **Encryption:** `encrypt: true` (NOT `'strict'`). Matches ODBC `Encrypt=yes`.
- **Auth:** `azure-active-directory-access-token` with `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 connection string |
| **Mirrored Database** | SQL analytics endpoint + item GUID (Portal → Settings) | Item GUID |

**Important:** For Lakehouse/Warehouse/Mirrored DB, you MUST pass the item GUID as `database`. Without it, multi-item workspaces cannot route correctly.

```ts
import sql from 'mssql';

// Always ask the user for these values — never invent them
const SQL_SERVER = '<endpoint>.datawarehouse.fabric.microsoft.com';
const DATABASE = '<item-guid-or-db-name>';

udf.func(
  'queryData',
  async (ctx: RayfinContext, query: string): Promise<Record<string, unknown>[]> => {
    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)

Ask the user for the file URL: Fabric Portal → Lakehouse → file → Properties → URL.

Format: `https://onelake.dfs.fabric.microsoft.com/<workspaceId>/<itemId>/Files/<path>`

```ts
udf.func(
  'readFile',
  async (ctx: RayfinContext, fileUrl: string): Promise<string> => {
    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 await res.text();
  },
  [udf.connection({ audienceType: AudienceType.Storage })],
);
```

### Azure Key Vault

Ask the user for the vault URL (e.g. `https://my-vault.vault.azure.net/`). Install: `@azure/keyvault-secrets`, `@azure/identity`.

```ts
import { SecretClient } from '@azure/keyvault-secrets';

udf.func(
  'getSecret',
  async (ctx: RayfinContext, kvUrl: string, secretName: string): Promise<string> => {
    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 (Cosmos DB, Blob Storage)

All follow the same `ContextTokenCredential` pattern — ask the user for the resource endpoint, wrap `ctx.getToken(AudienceType.X)`, and pass to the relevant Azure SDK client:

- **Cosmos DB** (`AudienceType.CosmosDB`): `new CosmosClient({ endpoint, aadCredentials: credential })` — install `@azure/cosmos`
- **Blob Storage** (`AudienceType.Storage`): `new BlobServiceClient(url, credential)` — install `@azure/storage-blob`

### Connection rules

- **Always ask the user for real endpoint URLs** — never invent them or use `process.env`
- Declare connections in the third argument to `udf.func()`
- Use `ctx.getToken(AudienceType.X)` — never acquire tokens manually
- Install SDK packages in `rayfin/functions/package.json`, not the root
- For SQL: use `mssql@^12.6.0` with `encrypt: true`

### Local development with delegated tokens

When `rayfin dev functions apply` is run and the host is started, the host uses `InteractiveBrowserCredential` to acquire tokens for all audiences for local debugging.

### Importing data entities

Use TypeScript project references to share entity types between data models and functions:

```ts
import type { TodoItem } from '../../data/TodoItem.js';
```

- Use `import type` — entity classes are only needed for their type shape, not runtime value.
- The `../../data/` path works because `tsconfig.json` has `"references": [{ "path": ".." }]` pointing to `rayfin/tsconfig.json`.
- Always use `.js` extension for ESM imports.

## Typegen

The Rayfin CLI parses all `udf.func()` calls in `rayfin/functions/src/` and generates `types.ts`:

```ts
export type AppFunctionsSchema = {
  greet: {
    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`.
- `rayfin dev functions apply` runs typegen once at startup and then keeps a visible watcher running in the same terminal (`[typegen]` prefix) until you Ctrl+C. There is no separate `typegen` CLI command.
- Never hand-edit `types.ts` — it is overwritten by typegen.

## Invoking Functions from Frontend

Import `AppFunctionsSchema` from the functions project and pass it to `RayfinClient`:

```ts
import { RayfinClient } from '@microsoft/rayfin-client';
import { resolveRayfinFunctionsBaseUrl } from '@microsoft/rayfin-local-dev';
import type { AppFunctionsSchema } from '../rayfin/functions/src/types.js';

const client = new RayfinClient<AppSchema, AppFunctionsSchema>({
  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
  functionsBaseUrl: resolveRayfinFunctionsBaseUrl(),
});

// Type-safe invocation — parameters and return type are checked
const greeting = await client.functions.greet.invoke({ firstName: 'Jane', lastName: 'Doe' });

// For void-input functions (RayfinContext-only)
const entries = await client.functions.getEntries.invoke();
```

- `invoke()` returns `Promise<TOutput>` directly (not an envelope).
- `functionsBaseUrl` is optional — when unset, function invocation uses the deployed Rayfin backend route.
- Bundled Vite templates include same-origin local routing already.
- `rayfin dev functions apply` auto-patches recognized existing client code on first run.
- See [Local Functions Debugging Setup](#local-functions-debugging-setup) if you need to configure a custom project manually.

## Local Functions Debugging Setup

Bare `rayfin dev` automatically:

1. Selects an available Functions port and writes its URL to `VITE_RAYFIN_FUNCTIONS_URL` through `.env.local`.
2. Starts the configured frontend and Functions host together.
3. Routes bundled Vite templates through same-origin `/.rayfin/api/<name>` requests.
4. Returns HTTP 502 if the local Functions host is unavailable instead of invoking deployed function code.

`rayfin dev functions apply` remains available as a compatibility entrypoint and uses the same runtime primitives.
When `VITE_RAYFIN_FUNCTIONS_URL` is unset or the application is built for production, `functionsBaseUrl` is `undefined` and function calls route through the deployed backend.

## Wiring `functionsBaseUrl` for Local Debug

Shipped Vite templates include `@microsoft/rayfin-local-dev`, register `rayfinLocalDev()` in `vite.config.ts`, and resolve `functionsBaseUrl` through the package's browser-safe entrypoint.
For a custom Vite project, add the same plugin before wiring the client:

```ts
import { rayfinLocalDev } from '@microsoft/rayfin-local-dev/vite';

export default defineConfig({
  plugins: [rayfinLocalDev()],
});
```

Then pass the same-origin base URL through the existing client configuration:

`src/services/rayfinClient.ts` — accept the new optional config field and pass it to `new RayfinClient(...)`:

```ts
export interface RayfinClientConfig {
  baseUrl: string;
  publishableKey: string;
  localDev: boolean;
  /** Local-debugging override. Set when `rayfin dev functions apply` is running. */
  functionsBaseUrl?: string;
}

client = new RayfinClient<AppSchema>({
  baseUrl: config.baseUrl,
  publishableKey: config.publishableKey,
  authStorage: true,
  functionsBaseUrl: config.functionsBaseUrl,
});
```

`src/services/bootstrap.ts` — pass the env var through, gated on `localDev` so production/Fabric-hosted bootstraps leave it `undefined` and the SDK uses its default routing:

```ts
import { resolveRayfinFunctionsBaseUrl } from '@microsoft/rayfin-local-dev';

const client = initRayfinClient({
  baseUrl: apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`,
  publishableKey: publishableKey ?? 'local-dev-key',
  localDev,
  functionsBaseUrl: resolveRayfinFunctionsBaseUrl(),
});
```

The env var `VITE_RAYFIN_FUNCTIONS_URL` is populated automatically while `rayfin dev` runs local Functions.
The CLI writes `RAYFIN_PUBLIC_FUNCTIONS_URL` into `rayfin/.env`, and the framework env projector exposes the Vite-prefixed variant.
For Next.js use `NEXT_PUBLIC_RAYFIN_FUNCTIONS_URL`; for other frameworks read the equivalent public-env-prefixed variable directly during development.

Idempotency: if `functionsBaseUrl` is already present in either file, leave it alone — the user (or a prior agent run) has already done the wiring.

## Rules

- Always register functions with `udf.func(name, handler, [])` — do not export bare functions.
- Use `RayfinContext<AppSchema>` for type-safe data access in functions. Bare `RayfinContext` (without a generic) still works but provides no type safety on `getDataClient()`.
- Keep `types.ts` auto-generated — do not add manual type definitions to it.
- Use `import type` for data entity imports in function files — runtime imports of `@microsoft/rayfin-core` decorators would pull in unnecessary dependencies.
- Functions must return a value or `void` — throwing an error surfaces as an invocation failure with the error message.
- `Promise<T>` return types are unwrapped to `T` in the generated schema.

## Anti-Patterns

- Never import `RayfinContext` from `@microsoft/rayfin-functions` — it comes from `@microsoft/fabric-user-data-functions`.
- Never hand-edit `rayfin/functions/src/types.ts` — it is regenerated by `rayfin functions init` (one-shot) and by the watcher inside `rayfin dev functions apply`.
- Never use `import` (non-type) for data entities in function files — use `import type` to avoid pulling decorator runtime into the functions bundle.
- Never expose a local Functions URL in a production build; leave `functionsBaseUrl` undefined so the SDK uses the deployed route.
- Never make `process.env` your primary production secret source for functions.
- Prefer secrets managed via `rayfin secret set <NAME>` and delegated auth connections via `udf.connection()`.
- Use `process.env` fallback only for local debugging, typically by mirroring keys in `rayfin/functions/local.settings.json`.

## CLI Quick Reference

```bash
# Scaffold
npx rayfin functions init              # Scaffold rayfin/functions/, install deps, generate types.ts
npx rayfin functions init --force      # Re-scaffold, overwriting existing files

# Local development (also runs the typegen watcher visibly in the same terminal)
npx rayfin dev functions apply         # Start local function host against active deployment
npx rayfin dev functions apply --port 7072           # Use a different port
npx rayfin dev functions apply --no-debug            # Disable Node.js inspector
npx rayfin dev functions apply --inspect-port 9230   # Custom inspector port
```

More from microsoft/aitour27-BRK390-building-ai-applications-with-microsoft-databases-and-fabric

SkillDescription
app-design>
app-validation>
caldova-campaign-planningEvaluate Hydration Sunscreen marketing investment at Caldova, compare regional campaign options, explain additional demand and supply requirements, approve a reviewed forecast, and prepare Karin's production capacity review. Use when asked where to invest campaign budget, which regions to prioritise, or whether production can support a proposed campaign.
caldova-demoUse when: configuring, deploying, rehearsing, debugging or changing the Caldova staffing demo, Azure Function or demo preflight.
dax-authoring>
demo-security-reviewUse when: reviewing demo security, publishing code, integrating demo artifacts, scanning for PII or secrets, or checking authentication and write safety.
fabric-cli>
fabric-sdk>
query-design>
rayfinUse when doing ANY task involving Rayfin — scaffolding, data models, decorators, auth, deployment, CLI commands, or client setup. Triggers: rayfin, rayfin init, rayfin up, rayfin login, RayfinClient, @entity, @role, @anonymous, @authenticated, @text, @uuid, @int, @decimal, @boolean, @date, @email, @set, @one, @many, DAB, Data API Builder, rayfin.yml, publishableKey, signUp, signIn, signOut, sendMagicLink, handleMagicLinkCallback, ensureSignedInWithFabric, Fabric SSO, Entra ID, rayfin up db apply, rayfin up staticapp deploy, schema.ts, OpaqueSession, onSessionChange, GraphQL select, GraphQL create, GraphQL update, GraphQL delete, findById, executePaginated, RLS policy, row-level security, claims.sub, claims.email, TC39 decorators, config generation, dialect, mssql, postgresql