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/.
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:
services:
storage:
enabled: trueFor 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 --helprayfin 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.
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
| Option | Type | Default | Description |
|---|---|---|---|
name | string | normalized class name | Folder 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. |
maxSize | number | ByteSize | service limit | Maximum object size. Use bytes(), kb(), mb(), or gb() helpers for typed byte values. |
allowedContentTypes | MimeGlob[] | any type | MIME 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 option | Where it used to appear | Replacement |
|---|---|---|
declaredSize | upload() options | The SDK infers size when it can; folder and service size limits are enforced by the service. Removed in 1.36 |
onProgress | upload() options | No SDK progress callback is exposed in 1.36. Removed in 1.36 |
userMetadata | upload() options | Declare typed fields on the folder class and pass them through fields. Removed in 1.36 |
per-call onConflict | upload() options | Set the folder default with @blob({ onConflict }). Removed in 1.36 |
contentType, contentDisposition, range | download() options | Download returns the headers OneLake supplies; the client does not request alternate headers or ranges. Removed in 1.36 |
correlationId | every operation options object | The 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.
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.
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.
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
| Operation | Options | Result |
|---|---|---|
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 } |
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 applyAdd --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.
}
}
}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.