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.
Choosing
Section titled “Choosing”| 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.
In the repository
Section titled “In the repository”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.
How an app reaches a host
Section titled “How an app reaches a host”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.
In a file store
Section titled “In a file store”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.
Behind an API
Section titled “Behind an API”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.
Querying data in the page
Section titled “Querying data in the page”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");