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