Developing for Stackboard
What Stackboard runs
Section titled “What Stackboard runs”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.
Project structure
Section titled “Project structure”The simplest app is three files at the root:
index.htmlstyles.cssapp.jsindex.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 Stackboarddist/ index.html entry path: dist/index.html assets/ app.js app.cssThe 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.
Frameworks
Section titled “Frameworks”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.
Assets and size
Section titled “Assets and size”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.
Environment variables
Section titled “Environment variables”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 and API targets
Section titled “Secrets and API targets”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.
Secrets in a request body
Section titled “Secrets in a request body”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.
Signing in
Section titled “Signing in”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.
Gotchas
Section titled “Gotchas”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.
