Skip to main content

Configure a Backing Store

A Backing Store is the destination (and, optionally, the source) that WorkbookIO uses to persist and load workbooks. It is the single place to redirect all I/O — a browser download, the local file system, a VSCode host, a database, or a cloud service.

SheetXL separates I/O into two independent dimensions:

DimensionWhat it controlsConfigured by
FormatHow the workbook is encoded (xlsx, csv, json, parquet…)The registered IO plugins (@sheetxl/io)
Backing StoreWhere the bytes go / come fromWorkbookIO.setBackingStore(...)

Because the two are independent, a custom store never has to know about formats. It only moves bytes — WorkbookIO resolves the format, suggested name, and destination Origin before the store is called, and hands the store a small helper (encode() / decode()) to do the actual encoding.

The default stores​

You usually don't configure anything. WorkbookIO auto-selects a built-in store for the current platform:

  • In the browser: BrowserBackingStore — uses the File System Access API (showSaveFilePicker / showOpenFilePicker) and falls back to a download / <input type="file">.
  • In Node: FSBackingStore — reads and writes local paths via fs.

The @sheetxl/sdk/lib/file entry point is platform-conditional — it resolves to the right store automatically, exposed under the DefaultBackingStore alias. You rarely import it directly:

// FSBackingStore in Node, BrowserBackingStore in the browser:
import { DefaultBackingStore } from '@sheetxl/sdk/lib/file';

The BackingStore interface​

A store only must implement write. Implementing read is optional — if you omit it, WorkbookIO.read falls back to the built-in platform store, so a "write-only" store still loads files normally.

namespace IWorkbookIO {
interface IBackingStore {
// Required. Persist the workbook described by the request.
// Return the written Origin (normally `request.origin`), or null to
// signal the user cancelled.
write(request: WriteRequest): Promise<Origin | null>;

// Optional. Acquire bytes and decode them into a workbook.
// Return null if the store can't produce one (e.g. picker cancelled).
read?(request: ReadRequest): Promise<IWorkbook | null>;
}
}

The write request​

Everything format-related is already resolved for you. The store's job is to take encode()'s bytes and put them somewhere.

interface WriteRequest {
workbook: IWorkbook; // the workbook being written
format: FormatType; // the resolved format (e.g. Excel) — key, extensions, mime
origin: Origin; // the destination handle (see below)
suggestedName: string; // a good default name *with* extension, e.g. "report.xlsx"
encode(format?: string): Promise<ArrayBufferLike>; // encode the workbook to bytes
}

The destination is an Origin — a virtual file handle. The store contract:

  • origin.getPath() set and origin.canWrite() true → persist to it silently (no dialog).
  • Otherwise (a brand-new workbook, or read-only provenance like a URL) → resolve a destination yourself — a save picker, a fixed location, an API endpoint — using suggestedName for any prompt or default. On success, fill the handle in (WorkbookOrigin.update) so the next save is silent, and return it.

The read request​

For reads, the store's only job is source acquisition — turn source (or, when it's null, a location it resolves itself, like an open picker) into bytes, then call decode(), which handles format detection, provenance (the workbook's origin), and workbook construction.

interface ReadRequest {
source: ReadOptions['source'] | null; // what to read, or null to resolve one yourself
options: Omit<ReadOptions, 'source'>;
decode(bytes: ArrayBufferLike, info?: ReadInfo): Promise<IWorkbook>;
}

Tell decode what you know about the source via ReadInfo — in particular canWrite: true when your store can silently write back to this source, so a plain write(workbook) re-saves in place.

Setting a store​

import { WorkbookIO } from '@sheetxl/sdk';

WorkbookIO.setBackingStore(myStore);

// Pass null to fall back to the built-in platform store:
WorkbookIO.setBackingStore(null);

After this, every WorkbookIO.write(...) (including a Studio Ctrl-S) routes through your store — you don't have to touch any command or UI code.

Reference implementation​

The smallest possible store is ArrayBackingStore (exported from @sheetxl/sdk). It captures the encoded bytes in memory instead of persisting them — a good template to copy:

import { type IWorkbookIO, type IWorkbook } from '@sheetxl/sdk';

class ArrayBackingStore implements IWorkbookIO.IBackingStore {
private _buffer: ArrayBufferLike | null = null;
private _formatKey: string | null = null;

async write(request: IWorkbookIO.WriteRequest): Promise<IWorkbookIO.Origin> {
const buffer = await request.encode(); // encode to the resolved format
this._buffer = buffer;
this._formatKey = request.format.key;
return request.origin;
}

async read(request: IWorkbookIO.ReadRequest): Promise<IWorkbook | null> {
if (!this._buffer) return null;
return request.decode(this._buffer, { format: this._formatKey ?? undefined });
}
}

Example: a backend / database store​

A common case is saving to your own service rather than the user's disk. Encode the workbook, PUT the bytes, fill the handle in, and return it:

import { WorkbookIO, WorkbookOrigin, type IWorkbookIO } from '@sheetxl/sdk';

class ApiBackingStore implements IWorkbookIO.IBackingStore {
constructor(private readonly endpoint: string) {}

async write(request: IWorkbookIO.WriteRequest): Promise<IWorkbookIO.Origin> {
const bytes = await request.encode();
// A bound handle re-saves to the same record; otherwise resolve a destination.
const name = request.origin.getPath() ?? request.suggestedName;

await fetch(`${this.endpoint}/${encodeURIComponent(name)}`, {
method: 'PUT',
headers: { 'Content-Type': request.format.mimeType },
body: bytes
});

// Fill the handle in: the next write(workbook) saves back here silently,
// and the workbook's name/size follow (the model observes its origin).
if (request.origin instanceof WorkbookOrigin) {
request.origin.update({ path: name, canWrite: true, size: bytes.byteLength });
}
return request.origin;
}

async read(request: IWorkbookIO.ReadRequest): Promise<IWorkbook | null> {
// `source` is whatever you passed to WorkbookIO.read({ source }) — e.g. an id.
const id = String(request.source ?? '');
const res = await fetch(`${this.endpoint}/${encodeURIComponent(id)}`);
if (!res.ok) return null;
const bytes = await res.arrayBuffer();
// Tell `decode` what it's looking at so it can pick the format — and that
// this store can write back to it (silent re-save on Ctrl-S).
return request.decode(bytes, { name: id, absolutePath: res.url, canWrite: true });
}
}

WorkbookIO.setBackingStore(new ApiBackingStore('https://api.example.com/workbooks'));
tip

You only need read if you also load through the store. If your app loads workbooks the normal way (URL, File, drag-drop) and only the save target is custom, implement write alone.


Example: a VSCode extension host​

In a VSCode extension the SheetXL UI typically runs in a webview, while the vscode API (including vscode.workspace.fs) is only available in the extension host. So the webview-side store doesn't touch the file system directly — it encodes the bytes and asks the host to write them over the message channel.

Webview side — a store that delegates to the host:

import { WorkbookIO, WorkbookOrigin, type IWorkbookIO } from '@sheetxl/sdk';

declare const acquireVsCodeApi: () => { postMessage(msg: unknown): void };
const vscodeApi = acquireVsCodeApi();

class VSCodeBackingStore implements IWorkbookIO.IBackingStore {
async write(request: IWorkbookIO.WriteRequest): Promise<IWorkbookIO.Origin | null> {
const bytes = await request.encode();
const name = request.origin.getPath() ?? request.suggestedName;

// Round-trip to the extension host, which performs the actual write.
const ok = await postAndWait('sheetxl:write', {
name,
bytes: new Uint8Array(bytes) // structured-cloned to the host
});
if (!ok) return null; // host reported the user cancelled

if (request.origin instanceof WorkbookOrigin) {
request.origin.update({ path: name, canWrite: true, size: bytes.byteLength });
}
return request.origin;
}
}

// Minimal request/response helper over postMessage.
function postAndWait(type: string, payload: unknown): Promise<any> {
return new Promise((resolve) => {
const id = crypto.randomUUID();
const onMessage = (e: MessageEvent) => {
if (e.data?.id !== id) return;
window.removeEventListener('message', onMessage);
resolve(e.data.result);
};
window.addEventListener('message', onMessage);
vscodeApi.postMessage({ id, type, payload });
});
}

WorkbookIO.setBackingStore(new VSCodeBackingStore());

Extension host side — receive the message and write with vscode.workspace.fs:

import * as vscode from 'vscode';

panel.webview.onDidReceiveMessage(async (msg) => {
if (msg.type !== 'sheetxl:write') return;

const uri = vscode.Uri.joinPath(workspaceFolder.uri, msg.payload.name);
try {
await vscode.workspace.fs.writeFile(uri, msg.payload.bytes);
panel.webview.postMessage({ id: msg.id, result: true });
} catch {
panel.webview.postMessage({ id: msg.id, result: false });
}
});
note

If your extension renders SheetXL directly in the extension host (rather than a webview) you can skip the message bridge and call vscode.workspace.fs straight from the store's write.

What you don't have to do​

WorkbookIO.write does all of this before your store runs, so your store stays small:

  • Resolves the write format (from the explicit format, then the destination's extension).
  • Resolves the destination Origin — every workbook carries its origin (where it was read from or last saved), so a plain write(workbook) hands your store the right handle; an explicit target (write(workbook, 'copy.xlsx')) hands you a handle for that target instead, and writeAs handles the "Save As" retargeting.
  • Computes a clean suggestedName (e.g. a workbook read from https://host/path/report.xlsx suggests report.xlsx).
tip

Full API reference: IWorkbookIO.