Skip to content

Adding data

Stackboard serves the files in your repository. It runs no database and no server of its own, so an app’s data comes from one of three places.

Where the data lives Use it when Size
In the repository It changes when your code does Up to 20 MB per file
In a file store It is large, or refreshed on its own schedule Unlimited
Behind an API It must be live, or differ per viewer Unlimited

Moving a file from your repository to a file store later means changing the address you fetch.

Commit the file beside your index.html and read it by name:

const res = await fetch('data.parquet');
const buffer = await res.arrayBuffer();

Stackboard serves it from the same commit the page was built from, so the data and the code that reads it stay in step. Each file may be up to 20 MB, counted separately from your entry document.

Two things to know:

  • Repository files are sent whole. There are no partial reads, so the browser downloads the entire file before your code sees any of it.
  • Git keeps every version. Replacing a data file adds to the history rather than replacing what was there, so a file rebuilt on a schedule grows the repository indefinitely.

An app cannot call arbitrary addresses. You list the ones it may reach as API targets, and each target records who makes the call:

Who calls
Stackboard (proxy) Stackboard makes the request and can attach a stored key, which never reaches the browser. Responses are fetched whole.
User’s browser (direct) The page calls the host itself, from the viewer’s machine. No size limit. Stackboard cannot attach your key, but each person can supply their own.

A direct target can still require a credential. Each person supplies their own token for it once, before the app launches, and Stackboard attaches it to their requests the same way the proxy would. Unlike a proxied key it does reach their browser, which is the trade for calling the host directly: it is their credential on their own machine rather than yours shared with everyone. Nobody else can read it, including you.

Put the file in S3, R2, Google Cloud Storage, or any host that serves files over HTTP, and add its address as a target called by the user’s browser (direct):

const res = await fetch('https://data.example.com/events.parquet');

The file does not pass through Stackboard, so the 20 MB limit does not apply. A file store can also return part of a file on request, which lets a query engine read only the bytes a query touches instead of downloading all of it first.

The store needs two settings of its own:

  • Browser access, usually labeled CORS. An app has no web address of its own, so it identifies itself as null.
  • Access control. Stackboard is not in the request and cannot restrict who reads the file. Use your store’s own signed links, or a token each person supplies, if the data is not public.

Add the API as a target called by Stackboard (proxy) and call it normally. Stackboard attaches the stored key after the request leaves the browser, so your code never holds it:

const res = await fetch('https://api.example.com/v1/sales');

See Secrets and API targets for storing the key.

An API can return different data to different viewers, because it decides what to send on each request. Responses are fetched whole, so this suits API payloads rather than large files.

For a few thousand rows of JSON or CSV, no engine is needed: parse the file and filter it in JavaScript. For SQL, or for files too large to hold in memory at once, a query engine can run in the browser itself, on the viewer’s machine.

DuckDB-Wasm Reads Parquet and CSV directly, and queries a file store in place, reading only the parts each query needs. The engine is around 35 MB, loaded from a CDN.
SQL.js SQLite compiled to WebAssembly, for data already in a SQLite file. Under 1 MB, small enough to commit to your repository.
Arquero Tables and grouping in plain JavaScript rather than SQL. No WebAssembly at all.

Whichever you use, one Stackboard rule applies. Scripts may load from any address, but fetches are restricted to declared targets, and an engine delivered as a script plus a separate WebAssembly file fetches that second file. Add the address you load it from as a target called by the user’s browser (direct), or loading fails as Failed to fetch. An engine small enough to commit avoids this, since a file in your repository is not an outside address.

The rest of this section uses DuckDB-Wasm. Pin a version rather than tracking the latest:

import * as duckdb from 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/+esm';
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
const worker = await duckdb.createWorker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);

A repository file arrives whole, so register it as a buffer:

const res = await fetch('events.parquet');
await db.registerFileBuffer('events.parquet', new Uint8Array(await res.arrayBuffer()));
const conn = await db.connect();
const result = await conn.query("select region, count(*) from 'events.parquet' group by 1");

A file store is queried in place, reading only the parts each query needs:

const conn = await db.connect();
const result = await conn.query(
"select region, count(*) from 'https://data.example.com/events.parquet' group by 1"
);