Store Builder

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 sb does is an HTTP request documented elsewhere — /app-docs for the app and its OAuth flow, /block-docs for 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-docssb 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 init makes no network calls; §2, §3 and §4 of /app-docs are 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_TOKEN is set in your environment, and the tool never stores a credential anywhere.
  • sb dev is 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 deploy is 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