Runtime environments and the end-to-end scenario
What changes between the desktop app, the browser, and the server, and what stays the same; the full "open bytes → edit → return XLSX" path.
What changes between environments
Section titled “What changes between environments”The SumSheet core runs in four environments. Only how a command reaches it changes; what it means and what the core answers is the same everywhere.
| Environment | Where the core runs | How the user controls it |
|---|---|---|
| Browser | Inside the page (WebAssembly). | Click, double-click, context menu, keyboard, selection. |
| Desktop app | A separate process on the computer. | The same actions as in the browser. |
| Phone and tablet | The mobile core is a library next to the app window (Phone and tablet); the app needs its own lifecycle. | Tap, double tap, and long press turn into the same actions. |
| Server and agents | A core process on the server, with no interface. | No clicks: requests and commands; command, session, and change events go out. |
A workbook checking service also lives next to the server core: it reads the workbook without executing anything and reports what in it will work — check workbooks before the pilot.
One difference doesn’t fit in the table: the core never goes to the network in any of these environments. It has no encrypted connection (TLS), and for an https request or a connection to Postgres and SQL Server it asks whoever is nearby to go — with a question back, to which it waits for an answer.
Hence the difference. In the desktop app there’s someone nearby to answer, and such operations work. On the server they work if whoever holds the process serves these questions: on its own, the process won’t go to the network. In the browser and on the phone there’s no one to answer, so network operations there honestly refuse — codes and the reason.
For the same reason, and in the same two environments, macros don’t run: they’re executed by a part of the core that exists in the desktop and server processor and is absent from the browser build and the mobile core. Project inspection, plan, and preview work everywhere — that’s parsing, not running (more).
The semantic contract is the same
Section titled “The semantic contract is the same”Selection, the context menu target, the operation, the revision, the result, and the saved file are all described the same way, regardless of how the transport is built. On a phone, a long-press turns into the same context-menu-opening event. On the server, there’s no mouse click, but an agent requests the same cell details and runs the same operation — with the same permissions and the same refusals.
Environment-specific limits
Section titled “Environment-specific limits”- In the browser, you can’t hand over the structure of a large workbook whole: every request is limited to a range.
- On a phone, the session must survive the app being evicted from memory — through a checkpoint and reopening, not through a reference to WebView memory that’s already gone by then.
- On the server, you need isolation between clients, quotas, task cancellation, and an action log. The delivery doesn’t include a ready-made server platform.
- An agent is only allowed a pre-agreed list of requests and commands. Credentials and the raw text of Power Query queries and macros never make it into the events.
For developers: the end-to-end scenario
Section titled “For developers: the end-to-end scenario”Next is the same “open bytes → read → edit → return XLSX” path in code, first for a separate process, then for the browser. If you’re not going to write code, you can close the page here: where exactly the core runs in each environment — in the browser, as a separate process, on the server.
Open, read, edit, save
Section titled “Open, read, edit, save”The example below uses the same protocol the build is verified with. Row and column indexes are zero-based. The viewport field limits the area that goes into the response, and sourceBytesBase64 lets you pass the workbook as bytes — then the core doesn’t need access to your file system.
Part 1: open the document and get its structure
import { spawn } from "node:child_process";import { readFile } from "node:fs/promises";import readline from "node:readline";
// The path to the processor inside the installed application is in the// F1_CLI environment variable; where it lives on each system is shown on// the "Installing SumSheet" page.const cli = spawn(process.env.F1_CLI, ["--session"], { stdio: ["pipe", "pipe", "inherit"] });const lines = readline.createInterface({ input: cli.stdout });const iterator = lines[Symbol.asyncIterator]();const next = async () => JSON.parse((await iterator.next()).value);const ask = async request => { cli.stdin.write(JSON.stringify(request) + "\n"); return next();};
const hello = await next();if (hello.kind !== "compute_contract_cli_session_ready") throw new Error("SumSheet core handshake failed");
const bytes = await readFile("./demo.xlsx");const viewport = { top: 0, left: 0, rows: 40, cols: 12 };const opened = await ask({ operation: "open_workbook_subset_from_bytes", sourceName: "demo.xlsx", sourceBytesBase64: bytes.toString("base64"), viewport, requestedBackend: "native-desktop"});if (!opened.ok) throw new Error(opened.code);
const { subsetId, activeSheet, objectInventory = [] } = opened.payload;console.log(activeSheet, objectInventory);Targeted edit and returning XLSX bytes
Section titled “Targeted edit and returning XLSX bytes”Once the workbook is open, the subsetId field names its live session in the core, and every following request carries this identifier. A change request passes expectedRevision: if the revision is stale, the core refuses before making the edit, rather than applying it halfway.
Part 2: addressed reading, an atomic edit, and saving
const cell = await ask({ operation: "read_cell_details", subsetId, row: 0, col: 0, viewport, requestedBackend: "native-desktop"});
const edited = await ask({ operation: "apply_cell_edits", subsetId, edits: [{ inputKind: "text", sheetKey: activeSheet.path, row: 1, col: 1, input: "Delivered" }], expectedRevision: 0, viewport, requestedBackend: "native-desktop"});if (!edited.ok) throw new Error(`${edited.code}: ${edited.message ?? ""}`);
const saved = await ask({ operation: "save_workbook_subset_to_bytes", subsetId, preserveCachedValues: true, requestedBackend: "native-desktop"});const output = Buffer.from(saved.payload.bytesBase64, "base64");
// Close the core: while the spawned process is alive, the program won't exit,// and the processor will keep hanging around after it ends.cli.stdin.end();cli.kill();The same contract in the browser
Section titled “The same contract in the browser”What a call to the core from the browser looks like
Section titled “What a call to the core from the browser looks like”The session in the browser: the page’s own code handles delivery, and the core keeps the document’s meaning.
The fastsheet_bridge_wasm.js module, next to the WebAssembly build of the core, is a separate delivery artifact: it ships as engine packages (see What is delivered) and isn’t part of the desktop installer. The processor paths from the installation page don’t apply here: those point to a native executable, while this is a browser module.
import init, { WasmComputeSession } from "./fastsheet_bridge_wasm.js";await init();
const core = new WasmComputeSession(Date.now(), 0x362cafe);const dispatch = request => JSON.parse(core.dispatch(JSON.stringify(request)));
const opened = dispatch({ operation: "open_workbook_subset_from_bytes", sourceName: file.name, sourceBytesBase64: await fileToBase64(file), viewport: { top: 0, left: 0, rows: 40, cols: 12 }, requestedBackend: "wasm"});What else the browser session can do
Section titled “What else the browser session can do”Besides dispatch, the session has three more methods, and the first one is almost always needed.
| Method | What it returns |
|---|---|
contract_version() |
The operations contract version, as a string. There’s no greeting line in the browser, so this is the only way to check compatibility. |
memory_telemetry() |
A JSON string about memory: how much is used right now, the peak of the last call, and the all-time peak. Diagnostics, not a contract operation. |
thread_pool_telemetry() |
A JSON string about worker threads — if the build is multithreaded and the browser granted them. |
simd_telemetry() |
A JSON string about vector computation: whether the module is built with SIMD and whether it responds with the check value. It shows which variant of the core loaded. |
This is exactly where it’s worth watching memory: it’s most tightly constrained in the browser, and memory_telemetry also shows the peak of the last call — that’s how you see which operation is expensive.
What to do with the current web shell
Section titled “What to do with the current web shell”Today the shell already sends the internal fastsheet:surface-object-selected event and handles click, double-click, and context-menu invocation itself. For a pilot, it’s worth hiding these behind one entry point. Subscribing directly, as shown below, is fine in an internal build — but don’t count on it as a long-term interface.
An internal event bridge — it still needs to be turned into a stable set of calls:
// INTERNAL / UNSTABLE: an adapter inside the SumSheet delivery.surface.addEventListener("fastsheet:surface-object-selected", event => { hostEvents.emit("objectSelected", { target: normalizeObjectTarget(event.detail), revision: currentRevision() });});
surface.addEventListener("dblclick", event => { const target = semanticHitTest(event.clientX, event.clientY); hostEvents.emit("doubleClicked", { target });});
surface.addEventListener("contextmenu", event => { event.preventDefault(); const target = semanticHitTest(event.clientX, event.clientY); openComposedMenu(target, hostMenuProviders);});- Exactly where the core runs in each environment — The core in the browser, a separate process, on the server.
- What’s locked in before integration — the Checklist.