The sb CLI
Do the same work from a terminal: the sb command scaffolds an app that runs, uploads an island's code every time you save, 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 — Build an app for the app and its OAuth flow, Build a block for blocks and islands. Nothing 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. If you have not built an app yet, start at
Build an app — 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 ships compiled — an installed copy has no
dependencies and nothing to build. The scaffold sb init writes is 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: it scaffolds, it does not register. Creating the app and its
version are still yours — §2 and §3 of Build an app.
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, islands or app data in the scaffold. Each is a real feature with its own guide, and none is needed to watch an app appear inside a merchant's admin for the first time.
The name is normalised into something npm accepts as a package name:
lower-cased, any run of characters outside a-z 0-9 . _ - collapsed to one
-, leading and trailing -_. trimmed. sb init "My App!" writes ./my-app;
with no name at all it also writes ./my-app.
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.
An existing directory is otherwise fine — running sb init inside a fresh
git clone is normal, and refusing it because .git exists would refuse the
common case.
| 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 |
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 outright.
3. sb.json — the file dev and deploy read
Both sb dev and sb deploy read sb.json from the current directory.
sb init does not write it, and cannot: three of its five fields are ids the
platform mints when you create the app and its draft version, which has not
happened at init time. Write it once you have them:
{
"orgId": "org_yourorg",
"appId": "app_1a2b3c4d",
"versionId": "apv_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".), 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.
sb deploy reads three more, all optional. Each one present becomes a step:
| Key | Type | What sb deploy does with it |
|---|---|---|
version |
object | PUT as the version's fields — scopes, embedUrl, redirectUri… |
blocks |
path | The block manifest, PUT as the body of .../blocks. See Build a block |
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_API=https://api.sbuilder.io.vn 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, then runs
until you interrupt it.
It is not hot reload. An island's code on a rendered page always comes from the platform's object store, so a page cannot be pointed at your dev server. What the command removes is the hunt for the endpoint: you save, it uploads, you refresh the page. It says exactly that, 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. §6 has that refusal.
sb dev takes no flags and uploads exactly one island. 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_API=https://api.sbuilder.io.vn sb deploy
SB_TOKEN=<your token> SB_API=https://api.sbuilder.io.vn 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 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 goes through several doors and no single call carries it. A draft is designed to be incoherent between writes: the coherence checks run at submit, not on each write, so a half-finished deploy is an unfinished version rather than a damaged one — 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.
| Flag | Effect |
|---|---|
--submit |
After the deploy succeeds, POST .../submit — the review gate in §8 of Build an app |
--submit first checks the version block of 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, never the authority. The server runs the real one, and if the two 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 |
The SB_API default is the address of a platform running on your own machine.
Against a real store, set it to https://api.sbuilder.io.vn.
SB_TOKEN is a token for the organization that owns the app — the same
user access token you put in Authorization: Bearer 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.
The refusals the tool translates rather than passing through:
| Server code | What sb says |
|---|---|
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 |
Each 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 —
§10 of Build an app and §9 of
Build a block tabulate them.
7. What it does not do
- It does not create the app, the version, or the install.
sb initmakes no network calls; §2, §3 and §4 of Build an app are still done by hand or by your own script. - It does not write
sb.json(§3), for the same reason. - There is no
sb login.SB_TOKENlives in your environment; the tool never stores a credential anywhere. sb devis not hot reload (§4), and cannot be while an island's code is served from the platform's object store.sb deployis not atomic (§5), deliberately.
Building the app itself: Build an app. Building a block or an island for the page editor: Build a block. Everything a token reaches once you hold one: Public API, or the API console to fire a request at a live server.
Updated 26/09/2026