Rayfin

Storage

Enable experimental Rayfin Storage, declare blob folders, configure object rules, and call the typed storage client.

Use Rayfin Storage when a Fabric app needs typed upload, download, list, and delete operations for objects declared in rayfin/storage/.

Experimental Changed in 1.36

Warning

Storage is experimental. Enable it only when your project explicitly needs Rayfin Storage, and confirm the service is available in your Fabric region and tenant before you build production flows around it.

Enable storage

Storage is enabled when either the project configuration opts in or the CLI feature flag is present. The project configuration is the persistent form:

rayfin/rayfin.yml
services:
  storage:
    enabled: true

For one-off CLI exploration, set the feature flag before running commands that expose storage subcommands or scaffold storage agent files:

$env:RAYFIN_FEATURE_FLAGS = 'storage'
npx rayfin dev --help

rayfin up storage apply, storage development commands, and the rayfin-storage agent skill are registered only when the storage feature resolves on. In 1.36.2 the resolver treats services.storage.enabled: true the same as RAYFIN_FEATURE_FLAGS=storage.

Declare a storage folder

Declare folders under rayfin/storage/. Import blob, StorageObject, byte-size helpers, and ContentTypes from @microsoft/rayfin-core/experimental; keep @role() and field decorators on the stable @microsoft/rayfin-core import.

rayfin/storage/ProfileImage.ts
import { role } from '@microsoft/rayfin-core';
import {
  blob,
  ContentTypes,
  mb,
  StorageObject,
} from '@microsoft/rayfin-core/experimental';

@blob({
  name: 'profile-images',
  onConflict: 'overwrite',
  maxSize: mb(5),
  allowedContentTypes: [ContentTypes.AnyImage],
})
@role('authenticated', '*', {
  policy: (claims, item) => claims.sub.eq(item.owner_id),
})
export class ProfileImage extends StorageObject {}

Every folder needs at least one authenticated permission rule. Anonymous storage permissions are not supported.

Folder options

OptionTypeDefaultDescription
namestringnormalized class nameFolder name, 3–63 lowercase letters, digits, or hyphens after normalization.
onConflict'error' | 'overwrite''error'error rejects an existing object with Conflict; overwrite replaces it and requires an authenticated role with update.
maxSizenumber | ByteSizeservice limitMaximum object size. Use bytes(), kb(), mb(), or gb() helpers for typed byte values.
allowedContentTypesMimeGlob[]any typeMIME globs such as 'image/*' or constants from ContentTypes. Unsupported uploads fail with UnsupportedContentType.

The 1.36 storage client removed unsupported per-call controls from upload and download options. If older code used them, move the intent into folder configuration or application logic.

Removed optionWhere it used to appearReplacement
declaredSizeupload() optionsThe SDK infers size when it can; folder and service size limits are enforced by the service. Removed in 1.36
onProgressupload() optionsNo SDK progress callback is exposed in 1.36. Removed in 1.36
userMetadataupload() optionsDeclare typed fields on the folder class and pass them through fields. Removed in 1.36
per-call onConflictupload() optionsSet the folder default with @blob({ onConflict }). Removed in 1.36
contentType, contentDisposition, rangedownload() optionsDownload returns the headers OneLake supplies; the client does not request alternate headers or ranges. Removed in 1.36
correlationIdevery operation options objectThe SDK creates and returns a correlation ID for each operation. Removed in 1.36

Register the storage schema

Map each schema key to the decorated class. If @blob({ name }) supplies an explicit folder name, the schema key must match that configured folder name exactly.

rayfin/storage/schema.ts
import { ProfileImage } from './ProfileImage.js';

export type AppStorageSchema = {
  'profile-images': ProfileImage;
};

export const schema = [ProfileImage];

Use bracket notation for keys that contain hyphens:

const images = await client.storage['profile-images'].list({ limit: 20 });

Do not intersect schema values with StorageObjectRef; the client adds the object envelope automatically and makes custom fields optional on returned objects.

Add storage to the client

Storage is composed through ExtendableRayfinClient and createStorageClient.

src/services/rayfinClient.ts
import type { RayfinClient } from '@microsoft/rayfin-client';
import { ExtendableRayfinClient } from '@microsoft/rayfin-client/experimental';
import {
  createStorageClient,
  type StorageClient,
} from '@microsoft/rayfin-storage';

import type { AppSchema } from '../../rayfin/data/schema';
import type { AppStorageSchema } from '../../rayfin/storage/schema';

export type AppClient = RayfinClient<AppSchema> & {
  storage: StorageClient<AppStorageSchema>;
};

export function createAppClient(baseUrl: string, publishableKey: string): AppClient {
  return ExtendableRayfinClient.create({
    baseUrl,
    publishableKey,
    schema: {} as AppSchema,
    services: {
      storage: createStorageClient<AppStorageSchema>,
    },
  });
}

Upload and download objects

Omit prefix by default. Add a prefix only when the application explicitly needs path partitioning, and use the same prefix for later list, download, and delete calls. Prefixes are organization, not authorization; the folder policy is the authorization boundary.

src/services/profileImages.ts
import type { StorageClient } from '@microsoft/rayfin-storage';

import type { AppStorageSchema } from '../../rayfin/storage/schema';

export class ProfileImageService {
  constructor(private readonly storage: StorageClient<AppStorageSchema>) {}

  async upload(userId: string, file: File) {
    return this.storage['profile-images'].upload('profile-image', file, {
      prefix: userId,
      contentType: file.type || 'application/octet-stream',
      fields: {},
    });
  }

  async list(userId: string) {
    return this.storage['profile-images'].list({ prefix: userId, limit: 20 });
  }

  async download(userId: string, name = 'profile-image') {
    return this.storage['profile-images'].download(name, { prefix: userId });
  }

  async remove(userId: string, name = 'profile-image') {
    return this.storage['profile-images'].delete(name, { prefix: userId });
  }
}

Use the signed-in session's subject identifier for userId; do not accept arbitrary user IDs from untrusted input.

Operation options and results

OperationOptionsResult
upload(name, data, options?)prefix, signal, contentType, contentDisposition, cacheControl, expiresInSeconds, fields{ object, correlationId }
download(name, options?)prefix, signal, expiresInSeconds{ stream, contentType?, contentDisposition?, contentLength?, etag?, correlationId }
list(options?)prefix, limit, continuation, signal{ items, continuation?, correlationId }
delete(name, options?)prefix, signal{ success, deleted, correlationId }

Changed in 1.36

The SDK flattens returned custom fields onto each listed object, so a team_id field declared on the folder class appears as item.team_id. Those custom properties are optional because individual objects may not contain every declared field.

Apply storage configuration

rayfin up applies enabled storage configuration during deployment. Use the cloud storage-only command for retries or storage-only changes:

npx rayfin up storage apply

Add --force only after reviewing and accepting destructive storage configuration changes. During npm run dev, restart the development session after changing storage declarations so the enabled project state is loaded before you retry storage operations.

Handle storage errors

Storage failures throw StorageError with code, status, correlationId, and retryable. The SDK does not automatically retry failed multi-step operations.

import { isStorageError } from '@microsoft/rayfin-storage';

try {
  await client.storage['profile-images'].upload('profile-image', file, {
    contentType: file.type,
  });
} catch (error) {
  if (!isStorageError(error)) throw error;

  switch (error.code) {
    case 'Conflict':
      break;
    case 'TooLarge':
      break;
    case 'UnsupportedContentType':
      break;
    case 'PermissionDenied':
    case 'Unauthenticated':
      break;
    default:
      if (error.retryable) {
        // Reissue the public SDK operation after backoff.
      }
  }
}
PromptAdd a Rayfin Storage folder
In my Rayfin project, enable Storage by setting services.storage.enabled: true in rayfin/rayfin.yml. Add rayfin/storage/ProfileImage.ts with @blob({ name: 'profile-images', maxSize: mb(5), allowedContentTypes: [ContentTypes.AnyImage] }) imported from @microsoft/rayfin-core/experimental, add an authenticated role policy comparing claims.sub to item.owner_id, register it in rayfin/storage/schema.ts with the exact 'profile-images' key, install @microsoft/rayfin-storage at the same exact version as the other @microsoft/rayfin-* packages, and apply the configuration with rayfin up storage apply.
Something wrong on this page?Report an issueEdit this page

On this page