Skip to content

Developing for Stackboard

Stackboard reads an entry document from your repository: index.html at the root by default, or any path you set. It inlines the local stylesheets and scripts that document names, rewrites the other asset URLs, and serves the result.

It does not run a build. Whatever is committed is what runs, so if your project has a build step, commit its output.

The simplest app is three files at the root:

index.html
styles.css
app.js

index.html links the other two the ordinary way, and Stackboard follows those links.

With a build step, commit the output folder and point the entry path at its index.html:

src/ source, ignored by Stackboard
dist/
index.html entry path: dist/index.html
assets/
app.js
app.css

The entry document’s folder is the site root. In dist/index.html, a reference to /assets/app.js means dist/assets/app.js, not the repository root. That’s what makes built output work unchanged.

Anything with a static adapter works, as long as the build output is committed:

Framework Setting Output
Vite default dist/
Astro default dist/
Create React app default build/
Next.js output: 'export' out/
SvelteKit adapter-static build/

Frameworks needing a Node server at runtime won’t run, since there is no server to run them on.

Plain HTML with a script tag is a perfectly good app. Don’t add a build step you don’t need, because every one of them is something to remember to re-run before committing.

Relative and root-relative paths resolve inside the repository. Absolute URLs are left alone, so libraries and fonts from a CDN load normally.

The entry document and the files it pulls in have a 20 MB budget, applied to each file and to the total. That is generous for hand-written code and easy to exceed by committing a large media file.

Write %VAR% anywhere in your source and Stackboard substitutes the stored value at launch:

const region = '%DEFAULT_REGION%';

A placeholder with no stored value is left as the literal text %VAR%, so a typo shows up on the page rather than silently becoming an empty string.

Substitution applies to public variables only. See Secrets.

Secrets are stored in Stackboard, never in the repository, and never reach the browser. Your code cannot read one, but it can use one.

Configure an API target: a name, an origin, which secret to attach, and whether it goes in a header or a query parameter. Restrict the methods and paths it accepts, since a target otherwise grants the whole origin.

Then call the API normally. Requests to a configured target’s origin are rerouted through Stackboard’s proxy, which attaches the credential after the request leaves the browser:

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

stackboard.fetch(target, path) does the same explicitly. Blocked calls are logged with a reason, so a too-narrow allowlist is easy to tell from code asking for something new.

Declare a variable without a value and each person supplies their own before the app launches. Those are visible to nobody else, including you.

Some APIs take credentials in the body, such as a login call that exchanges a username and password for a token. On the target, under Attach secrets, choose those secrets in In the request body, list the paths they may be filled on, and write them as %VAR% in the body:

await fetch('https://bi.example.com/api/auth/login', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
username: '%BI_USERNAME%',
password: '%BI_PASSWORD%',
}),
});

The proxy fills them in on the way out, escaped for JSON, form, and text bodies. Other content types, other paths, and secrets not chosen are sent as written. These requests are never cached. Whatever the API sends back, such as a token, reaches your code as usual.

Keep the path list to endpoints that need it. One that echoes its request back would hand the secret to the page.

If an API gives a session token after signing in, let Stackboard do the sign-in instead of your code. Under Sign in first, set:

  • Sign-in path: where to post, such as api/auth/login.
  • Sign-in body: written with %VAR% for secrets. It’s sent as JSON when it starts with {, and as form fields otherwise.
  • Session token from: a response header, a field in the JSON response (dots reach into objects, as in data.token), or cookies only.
  • Send the token in header, with an optional prefix such as Bearer .

The proxy signs in before the first call and attaches the token and cookies to every request. It signs in again when a call returns 401. Your code makes plain calls, and neither the password nor the token reaches the page:

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

Users whose secrets have the same values share one session. Cached responses are keyed on the sign-in, not the token, so they’re shared the same way and survive a new sign-in. When each user supplies their own secrets, each gets their own session and cache.

A secret written as %VAR% is not substituted in your code. Stackboard leaves it as the literal placeholder on purpose. If a key is missing at runtime, that’s why: the code is trying to inline something it must reach through a target instead. The one exception is a request body sent through a target set up to fill it.

Local development needs its own arrangement. Use a .env file, a dev fallback, or a local proxy, and replace it with %VAR% and an API target for Stackboard. Never commit the .env.

Only configured targets are reachable. The page runs sandboxed, and network access is restricted to Stackboard and the origins you have added as targets. A fetch to an API with no target fails, however permissive that API’s CORS headers are.

Browser storage is unavailable. The sandbox gives the page an opaque origin, so localStorage, sessionStorage, and cookies don’t work.

Stale build output. Since Stackboard runs what’s committed, forgetting to rebuild before committing ships the previous version. Building in CI and committing the result avoids this.

A path that’s right locally can be wrong here. Check that your entry document’s links work when its own folder is the site root.