Store Builder

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 sb does 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 init makes 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_TOKEN lives in your environment; the tool never stores a credential anywhere.
  • sb dev is not hot reload (§4), and cannot be while an island's code is served from the platform's object store.
  • sb deploy is 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