Skip to content
Query.Farm
Talk with Us

Runtimes & entry points

The TypeScript SDK is the only VGI SDK that runs inside the same places the engine can run: a server, a Cloudflare Worker, or a browser tab. That reach costs one piece of ceremony — picking the right subpath export — and this page is about which one.

ImportForContains
@query-farm/vgiNode / Bun workersEverything: the function factories, Worker, catalogs, the client.
@query-farm/vgi/serveAn HTTP worker on BunserveVgiWorker, createVgiWorkerFetch.
@query-farm/vgi/worker-cfCloudflare WorkerscreateVgiFetch plus the factories, on the flechette backend.
@query-farm/vgi/clientCalling a worker from a browserClient only — no server-side code in the bundle.
On Cloudflare Workers, import from `/worker-cf` — not the package root

This is not a stylistic preference. The root entry includes Worker, whose AF_UNIX transport pulls in node:net; the worker-cf entry omits it so that never enters the bundle. The subpath is also what selects the right Arrow backend through the package’s conditional exports — import the root on workerd and you get a bundle that either fails to build or links the wrong Arrow.

The SDK ships a backend-agnostic Arrow facade and resolves an implementation at build time, per runtime:

RuntimeArrow backendWhy
Node.js / Bun@query-farm/apache-arrow (arrow-js)The peer dependency you already installed.
Cloudflare Workers (workerd)@query-farm/flechetteA worker exercises much of the Arrow API and cannot take a peer dep.
Browser@query-farm/apache-arrow (arrow-js)It tree-shakes to the subset the client touches — measurably smaller.

The browser row is the surprising one, and it is a measurement rather than a guess: on the client surface a minified browser bundle is 84 KB gzip via arrow-js against 116 KB via flechette, because arrow-js shakes down to what the client uses while flechette links as a unit. Flechette still wins for worker-cf, which touches far more of the API.

The facade is the reason your code is portable

Because the backend is chosen at build time, your function bodies never name an Arrow implementation — which is exactly why they run unchanged on a server and on workerd. The cost is the erased column type: VgiColumn.get() returns unknown because the two backends disagree on what it would otherwise be. See Value representations.

createVgiFetch returns the fetch handler the Workers runtime expects:

// src/index.ts
import { createVgiFetch, defineScalarFunction, FunctionRegistry, ReadOnlyCatalogInterface, int64 } from "@query-farm/vgi/worker-cf";

const double = defineScalarFunction({
name: "double",
params: { n: int64() },
returns: int64(),
compute: (batch) => {
  const ns = batch.getChildAt(0)! as Iterable<bigint | null>;
  return Array.from(ns, (v) => (v == null ? null : v * 2n));
},
});

const registry = new FunctionRegistry();
const catalogInterface = new ReadOnlyCatalogInterface(
{ name: "calc", schemas: [{ name: "main", functions: [double] }] },
registry,
);

export default {
fetch: createVgiFetch({
  registry,
  catalogInterface,
  landingInfo: { name: "calc", doc: "Doubling, at the edge.", version: "0.1.0" },
}),
};
`landingInfo` is required here, and only here

createVgiFetch demands landingInfo: { name, doc, version } — the identity the landing page and its JSON status document display. serveVgiWorker builds it from its own required name/doc/ version, which is why only the Cloudflare entry passes it explicitly. Omit it and there is no landing page and no /vgi-client.js.

`int` means different things on the two entry points

Note int64() above rather than the bare int the tutorial uses. The two entries resolve that name differently:

Import fromint is
@query-farm/vgian Int64 instance (re-exported from vgi-rpc) — use it bare: { n: int }
@query-farm/vgi/worker-cfthe typed factory int(bitWidth?, signed?) — must be called: { n: int() }

The same collision applies to int32, float32 and bool. So a function that works on a server can register a nonsense argument type after nothing more than a changed import line.

int64() is the portable spelling — a factory on both entries, so it means the same thing everywhere. Prefer it, along with utf8(), float64() and the rest of the explicit factory set, in any code you intend to move between runtimes.

On 0.28.0 the mistake is silent: the argument registers with a Function as its type and the failure surfaces later, at bind. From 0.29.0 it is rejected when the function is defined:

defineScalarFunction("double"): params.n is a type factory, not an Arrow type — call it: int64().

Deployed, it attaches like any other HTTP worker:

ATTACH 'calc' (TYPE vgi, LOCATION 'https://calc.your-account.workers.dev');
Set the signing key as a Workers secret

A Cloudflare Worker is many isolates by construction, so the ephemeral-key failure mode described in Serve over HTTP is not a maybe — it is the default outcome. Set VGI_SIGNING_KEY with wrangler secret put before the first real query.

A worker that needs state across requests — a buffering function, a work queue — has no filesystem on workerd. FunctionStorageCfDo implements the storage interface over a Durable Object, so the same functions work there. See State storage.

Two different things can happen in a browser, and it is worth keeping them apart:

  • Calling a remote worker — import @query-farm/vgi/client and talk to an HTTP worker over fetch. This is what the landing page’s own /vgi-client.js does.
  • Running the engine itself@haybarn/haybarn-wasm is DuckDB compiled to Wasm with the vgi extension available, so a page can run queries locally that attach a remote worker.

The SDK’s browser export condition serves the first. The second is Haybarn’s job.

Node.js 22.15+ or Bun. The difference that shows up in practice is the tutorial’s LOCATION: Bun runs TypeScript directly, so LOCATION 'bun run ./worker.ts' needs no build. On Node you either run a compiled .js, or use Node’s own type stripping:

ATTACH 'calc' (TYPE vgi, LOCATION 'node --experimental-strip-types ./worker.ts');

serveVgiWorker is Bun-only because it calls Bun.serve. On Node, use createVgiWorkerFetch and bind the port with whatever HTTP server you already run.