Develop locally
shop dev runs a tenant's app, and shop-ops dev --service a
service of your product, on your machine as a deploy would run it: scaledbox's
runtime, its compile step and the platform's own parts, with a local stand-in for every binding. Try a
change in a fraction of a second, debug it with your own tools, and deploy only what works. Your
product's tools (an app builder, a test suite) drive it through an SDK.
Run an app
The directory is the one you deploy: box.toml at its root and the JavaScript modules your build made.
shop dev build/acme-shop --assets build/acme-shop/public
local.local.acme-shop.dev answers at http://127.0.0.1:8787
release 3f1c09a2b7e4 (3 modules, 12 assets), started in 380 ms
control http://127.0.0.1:53211, state build/acme-shop/.scaledbox/dev
database postgres://sbx_local_acme_shop_dev:…@127.0.0.1:5432/sbx_local_acme_shop_dev
GET /api/orders 200 3.1ms
[log] order 42 paid
Each request, console.log and error is a line (an error's stack points at your sources when your
build wrote source maps). Change a file and the app is built and deployed again, as a deploy is: its
new modules compiled, a box of the release woken and checked, then the app moved to it. A change that
doesn't compile, or whose top level throws, leaves the running release and says why.
It is the deploy's code, not an imitation of it, so the checks and limits hold as deployed: box.toml
is read as the API reads it, a release is compiled by the same compiler, the paths run_worker_first
names reach your fetch and the rest your assets (a single-page app's navigation gets its
index.html), and your tenants' default limits apply (CPU per invocation, cron's interval, a box's
memory).
| Option | |
|---|---|
--assets <dir> |
the directory of its static assets |
--port <n> |
its port on 127.0.0.1 (default 8787; 0: any free one) |
--database-url <url> |
a local Postgres to use, as a superuser (default: one embedded, below) |
--state <dir> |
where its bindings keep their data (default <dir>/.scaledbox/dev) |
--project, --tenant, --app, --env |
the names its box has (default local, local, the directory's name, dev) |
--no-watch |
don't rebuild on changes |
--json |
events as JSON lines, for tools (below) |
--runtime <file> |
the runtime binary to run (default SCALEDBOX_RUNTIME; below) |
--service <name> |
shop-ops dev only: a service of your product instead (below), --spec its scaledbox.toml (default ./scaledbox.toml) |
--outbound <host>=<url> |
a host box.toml's hosts names, its fetches sent to a server of yours (below) |
The runtime
Your CLIs are built from the module and carry no runtime of their own: name one with --runtime or
SCALEDBOX_RUNTIME. Your servers run the runtime of the scaledbox version in your go.mod; build that
one from scaledbox's source at that version (cargo build --release -p runtime, in runtime/) to run
exactly what they run. Or install scaledbox's own CLI (curl -fsSL https://scaledbox.dev/install.sh | sh): its first scaledbox dev downloads the latest scaledbox's runtime (a few megabytes, checked
against the digest it carries) into your cache directory, as the file
scaledbox/runtime/<version>/runtime, which SCALEDBOX_RUNTIME may name.
Run a service of your product
npm run build # your build writes dist/web: box.toml and its modules
shop-ops dev --service web
runs the service [services.web] of the scaledbox.toml in this directory as shop-ops deploy --env dev
would run it: its dir and assets, its health, memory, replicas and env (with [env.dev]'s
overrides; --env staging takes staging's), its box.toml checked as a product's service's (a binding a
product can't have, or limits beyond a product's, are refused as a deploy refuses them). Its variables are
strings in env, as deployed: SCALEDBOX_PROJECT (your [app] name), SCALEDBOX_ENVIRONMENT,
SCALEDBOX_SERVICE, its env, and your environment's secrets from .dev.vars beside scaledbox.toml.
Before each release goes live, if it is [release]'s service, its release() handler runs in a box of
it (event.release numbers this machine's releases, event.first until one went live), then its
health path is asked; a release that fails either keeps the running one. A change of .dev.vars, or of
the service's settings in scaledbox.toml (read again at each change), is a release too, its release
handler run again, as shop-ops secrets set or a deploy rolls one out. Your build may remove and make
its directory again: the repository is watched (but hidden directories and node_modules). Its state is kept in
.scaledbox/dev/<service> beside scaledbox.toml (its database sbx_sbx_product_<service>_dev).
shop-ops, built from the module, downloads the runtime of the scaledbox
release it is built from (the commit or version in your go.mod), once, from
https://scaledbox.dev/cli/<release>/, checks it against the release's runtimes.note (the runtimes'
SHA-256s, signed with a key the module trusts) and keeps both under your cache directory's
scaledbox/runtime/<release>/; a run after the first needs no network. --runtime or
SCALEDBOX_RUNTIME names another runtime binary.
Stand-ins for the APIs it calls
shop-ops dev --service web --outbound api.stripe.com=http://127.0.0.1:9000
sends what your code fetches from https://api.stripe.com/... to http://127.0.0.1:9000/... (the same
path, query, method, headers and body), a server of yours standing in for it, as Miniflare's
outboundService does. The host must be one of box.toml's hosts (a fetch is checked as deployed
first), and the stand-in this machine's (127.0.0.1, localhost, [::1]). A deploy has no such
thing: deployed, your code reaches the host itself.
Its bindings
| Binding | Locally |
|---|---|
database, kv, queue, workflow, objects, auth's users |
the tenant's own database on a local Postgres, with scaledbox's schema installed as on the platform: open it with psql (its URL is printed) and see your kv, queues and objects as tables. Without --database-url (or SHOP_DEV_DATABASE_URL), a Postgres 18 embedded in the state directory, downloaded once (about 30 s the first time) |
bucket |
files under the state directory, buckets/scaledbox-local/tenants/<tenant>/<binding>/<key>, through the same S3 gateway and gate as the platform's (presigned URLs, size limits, bucket events) |
secret |
<dir>/.dev.vars, one NAME=value a line (read again at each change) |
webhooks |
Svix's API for your app: endpoints you create get your messages, signed as Svix signs them |
ai |
the gateway with a mock provider (any model mock/<name>; a message with @mock tokens=50 streams 50 tokens), and openai, anthropic or google when OPENAI_API_KEY, ANTHROPIC_API_KEY or GEMINI_API_KEY is set |
auth |
sign-up and sign-in on your app's own /api/auth/*, as deployed; emails (codes, invitations) are printed instead of sent |
cron |
fires as deployed (at most every tenant's cron interval, default an hour); POST /scheduled runs it now |
The state directory keeps everything between runs; delete it to start afresh.
From your own tools
TypeScript and JavaScript: @scaledbox/dev starts your CLI's dev and drives it (scaledbox: "shop"
names your CLI; SCALEDBOX_CLI too).
import { start } from "@scaledbox/dev";
const dev = await start({ scaledbox: "shop", dir: "build/acme-shop", assets: "build/acme-shop/public", onLog: (l) => console.log(l.msg) });
const res = await dev.fetch("/api/orders"); // a request to the app
await dev.reload(); // after your tool rebuilt the directory
const { bindings } = await dev.state(); // what each binding holds
await dev.scheduled(); // run scheduled() now
await dev.stop();
reload() fails with the compiler's message when a module doesn't compile, the running release
staying. start({ scaledbox: "shop-ops", service: "web", outbound: { "api.stripe.com": "http://127.0.0.1:9000" } }) runs a
service of your product instead (--service, --outbound). Should your process die, the runner and its boxes stop with it.
Any other language: run shop dev --json. Each line on stdout is an event; the first is
{"event":"ready","url":"http://127.0.0.1:8787","control":"http://127.0.0.1:53211","state":{…}}
then log (a line the app wrote), reload and error (a rebuild after a change). The control URL
answers GET /state, POST /reload, POST /scheduled?cron=<schedule> and POST /stop. Keep a pipe
on its stdin: when it closes, the runner stops.
What differs from the platform
- No sandbox: the runtime runs as a process of yours (its own guard still holds
fetchto the hostsbox.tomlnames, and each invocation to its CPU time). - One machine: no other nodes, and nothing is metered or billed.
- The database is reached directly, not through a connection pooler and TLS.
- Webhook deliveries are tried three times over about 35 seconds, not for days.