Develop locally
scaledbox dev runs a tenant's app, or a service of your product,
on your machine as a deploy would run it:
scaledbox's own 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.
scaledbox 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).
The first run downloads the runtime of your CLI's version (a few megabytes, checked against the digest your CLI carries) into your user cache.
| 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) |
--service <name> |
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) |
Run a service of your product
npm run build # your build writes dist/web: box.toml and its modules
scaledbox dev --service web
runs the service [services.web] of the scaledbox.toml in this directory as scaledbox 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 scaledbox 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).
Stand-ins for the APIs it calls
scaledbox 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 SCALEDBOX_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 scaledbox dev and drives it.
import { start } from "@scaledbox/dev";
const dev = await start({ 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({ 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 scaledbox 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.