The sb CLI
Do the same work from a terminal: the sb command scaffolds an app, runs it against a real store, and pushes a version without opening the browser.
sb is a small command-line tool that does the mechanical parts of building an
app: it writes a working app you can run, it uploads an island's code while you
edit it, and it pushes a version's payload in the order the platform accepts.
It is a faster path through the same territory, not a different one. Everything
sbdoes is an HTTP request documented elsewhere —/app-docsfor the app and its OAuth flow,/block-docsfor blocks and islands. Nothing here is reachable only through the CLI, and nothing here hides a request you cannot make yourself. When a command refuses, the sentence you need is usually on one of those two pages; this one tells you what the tool did before it stopped.
There are three commands, and this page is the reference for all three. If you
have not built an app yet, start at /app-docs — sb assumes you
already have an organization and an app.
1. Getting it
@sbuilder/cli is on npm. Nothing to install:
npx @sbuilder/cli --help
or put it on your PATH for good:
npm install -g @sbuilder/cli
sb --help
It needs Node 20 or newer.
The CLI is written in TypeScript and ships compiled: an installed copy has no dependencies and nothing to build.
The scaffold sb init writes is deliberately the same way — no dependencies,
no build, plain ESM — because that is the code you actually run.
2. sb init — scaffold an app that runs
sb init my-app
sb init my-app --port 4000
Writes a working app into ./my-app and prints what to do next. It makes no
network calls at all: it is a scaffolder, not a registration step. Creating the
app and creating a version are still yours to do, and they are §2 and §3 of
/app-docs.
What it writes — five files, and nothing else:
| File | What it is |
|---|---|
server.js |
The whole app: the /oauth/callback token exchange and the framed /embed page, in one file with no dependencies |
package.json |
npm start runs server.js. No dependencies and no devDependencies — the first npm install should not be the first thing that can fail |
.env.example |
The two values the platform gave you (SB_CLIENT_ID, SB_CLIENT_SECRET) plus SB_API, SB_REDIRECT_URI and PORT |
README.md |
The same next steps the command prints, for when the terminal has scrolled |
.gitignore |
node_modules and .env |
There are deliberately no blocks, no islands and no app data in the scaffold. Each is a real feature with its own guide, and none of them is needed to watch an app appear inside a merchant's admin for the first time.
The name
The argument is normalised into something npm will accept as a package name:
lower-cased, with any run of characters outside a-z 0-9 . _ - collapsed to a
single -, and leading/trailing -_. trimmed. sb init "My App!" writes
./my-app. With no name at all it writes ./my-app.
What it refuses
It never overwrites, and it never half-writes. Before anything is written it checks the whole file set against the target directory; a single collision refuses the lot and names what clashed:
/path/to/my-app already contains package.json, server.js — refusing to overwrite.
Pick an empty directory, or move those files aside.
A partial scaffold is worse than none, because it leaves a directory that looks
initialised and is not. An existing directory is otherwise fine — running
sb init inside a fresh git clone is a normal thing to do, and refusing it
because .git exists would refuse the common case.
sb init flags
| Flag | Default | Effect |
|---|---|---|
--port <n> |
3000 |
The port baked into .env.example, the README and every URL the command prints. A value that is not a positive number is refused with --port must be a number |
The thing it tells you that is documented nowhere else
No tunnel. A draft version may point at http://localhost, and a sandbox
install may frame it, so the entire development loop runs on your own machine. A
tunnel is only for checking the public URLs before you submit. This is the fact
that most often costs a first-time developer an hour, so the command says it
rather than leaving it to §1 of /app-docs.
3. sb.json — the file dev and deploy read
Both sb dev and sb deploy read a sb.json from the current directory.
sb init does not write it, and it cannot: three of its five fields are ids
the platform mints when you create the app and its draft version, which has not
happened yet at init time. Write it by hand once you have them:
{
"orgId": "org_yourorg",
"appId": "app_1a2b3c4d",
"versionId": "ver_5e6f7a8b",
"island": "countdown",
"entry": "islands/countdown.js"
}
Those five keys are required by both commands — a missing one is refused by
name (sb.json is missing "versionId".), and a missing file is refused with the
list of what it needs. sb deploy needs them even though its own work is
described by the optional keys below, so fill them in even if you are only
deploying blocks.
sb deploy reads three more, all optional. Each one it finds becomes a step;
each one it does not find is skipped:
| Key | Type | What sb deploy does with it |
|---|---|---|
version |
object | PUT as the version's fields — scopes, embedUrl, redirectUri and the rest |
blocks |
path | The block manifest, PUT as the body of .../blocks. See /block-docs |
islands |
array of {name, entry} |
Declares every name, then uploads every entry |
A sb.json with only the five required keys is valid: sb dev works, and
sb deploy finds no steps to run.
4. sb dev — save, upload, refresh
SB_TOKEN=<your token> sb dev
Watches the entry file from sb.json and uploads it to the version's island
every time you save. It uploads once immediately on start rather than waiting for
your first save, then runs until you interrupt it.
It is not hot reload, and it will never say it is. An island's code on a rendered page always comes from the platform's object store, so a page cannot be pointed at a local dev server and there is no live patching to be had. What the command removes is the friction of finding the endpoint: you save, it uploads, you refresh the page. It says exactly that in the terminal, each time:
uploaded countdown — refresh the page to see it
It uploads to a draft. A version that has left draft refuses the write — approval pins a payload, and an island swapped after review is code running on merchants' pages that nobody looked at. See §6 for what that refusal looks like.
sb dev takes no flags. It uploads one island — the one named in sb.json. To
push several at once, use sb deploy with an islands array.
5. sb deploy — the whole payload, in dependency order
SB_TOKEN=<your token> sb deploy
SB_TOKEN=<your token> sb deploy --submit
Runs every step your sb.json describes, in this order:
version fields PUT /api/orgs/{orgId}/apps/{appId}/versions/{versionId}
blocks PUT .../blocks
island declarations PUT .../islands
island "<name>" POST .../islands/module?name=<name> (one per island)
The order is the design. Declarations come before the modules that fill them,
because code uploaded for an island the version does not declare is refused
(island_not_found), while a declaration with no code yet is a state the platform
expects. A failure part-way therefore leaves the shape of what you intended
rather than orphaned bytes.
It is not atomic, and it does not need to be
A version's payload is written through five doors and no single call carries it, so a tool that sequenced them and called the result atomic would be making a promise in the one place you would rely on it. It does not need the promise: a draft is designed to be incoherent between writes. Both coherence checks run at submit, not on the individual writes, so a half-finished deploy is an unfinished version rather than a damaged one — and the cost is precise: it cannot be submitted until it is finished, and the submit refusal names exactly what is missing.
What the command owes you instead is to say where it stopped:
Stopped at: island "countdown"
This version is in review or already approved, so its code is frozen.
Create a new draft version and point sb dev at that one.
(server: version has left draft)
Already applied: version fields, blocks, island declarations
This left the version UNFINISHED, not broken — a draft is allowed to be
partway through, and it simply cannot be submitted until the rest lands.
Fix the cause and run sb deploy again; every step is safe to repeat.
Every write is idempotent, so resuming is simply running it again.
sb deploy flags
| Flag | Effect |
|---|---|
--submit |
After the deploy succeeds, POST .../submit — the same review gate as §8 of /app-docs |
--submit first checks the version block in your sb.json for URLs still
pointing at your own machine, and refuses before it calls the server:
This version still points at your own machine: embedUrl (http://localhost:3000/embed).
Review needs public https URLs — the loopback allowance covers development only.
Update the version to its real addresses, deploy again, then submit.
That check is a courtesy and never the authority. The server runs the real one, and if the two ever disagree the server wins; this exists only to fail earlier, with more room to explain than a status code has.
6. Environment, and what refuses
Two variables, and only two:
| Variable | Required by | Default |
|---|---|---|
SB_TOKEN |
sb dev, sb deploy |
none — both stop immediately without it |
SB_API |
sb dev, sb deploy |
http://localhost:8080 |
SB_TOKEN is a token for the organization that owns the app — the same user
access token you would put in an Authorization: Bearer header against the
org-scoped routes. It is not a wba_ app token; those belong to an install, and
an install cannot edit the app it is an install of.
Every command exits 0 on success and 1 on a refusal it can explain. Anything
else is a bug in the tool and gets a stack trace, because a scaffolder that
swallows its own failures is one nobody can report.
The refusals the tool translates, rather than passing through verbatim:
| Server code | What sb says instead |
|---|---|
not_draft |
This version is in review or already approved, so its code is frozen — create a new draft and point at that one |
island_not_found |
This version does not declare an island by that name; declare it first |
island_module_not_text |
That file is not text. An island module is JavaScript |
island_module_too_large |
That file is too large to upload as an island module |
unauthorized |
Not signed in. Set SB_TOKEN to a token for the organization that owns this app |
any other 401/403 |
This token cannot edit that app — check SB_TOKEN, and that the app belongs to the organization you named |
Every one of them keeps the server's own sentence in (server: …) and adds the
next action. A code the tool has not been taught is printed in the server's words
rather than a shrug — /app-docs §10 and /block-docs
tabulate those.
7. What it does not do
Stated so you can plan around it rather than discover it.
- It does not create the app, the version, or the install.
sb initmakes no network calls; §2, §3 and §4 of/app-docsare still done by hand or by your own script. The CLI starts being useful once those ids exist. - It does not write
sb.json(§3), for the same reason. - There is no
sb login.SB_TOKENis set in your environment, and the tool never stores a credential anywhere. sb devis not hot reload (§4), and no future version of it can be while an island's code is served from the platform's object store.sb deployis not atomic (§5), deliberately.- It is not on npm yet (§1).
Building the app itself: /app-docs. Building a block or an island
for the page editor: /block-docs. Everything a token reaches once
you hold one: /api-docs, or the API
console to fire a request at a live server.
Updated 22/08/2026