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