Development, preview and production are configurations of a key, not deployments. A development key runs on your own machine and cannot spend money by construction.
Three environments, and the environment is a property of the key:
|
Compute |
Purpose |
development |
Your own machine |
Build and iterate. Never spends money |
preview |
A service identity's machine, or your app's own fallback with a hard cap on runs handed back |
Rehearse the configuration. CI |
production |
Whatever the end user's plan permits |
Serve real users |
A .env.local holds a development key; your production environment variables hold a
production key. That is the whole mechanism. Soba deploys nothing and there is no
soba deploy, because a second key already buys everything such a command could.
Why this is a correctness feature, not a tier#
Without it, a key used from a laptop during development is indistinguishable from the
key serving real users. A developer who pairs their own machine and tests against it
creates a user, a machine and a stream of runs served at user-subscription, which is
arithmetically identical to a real customer connecting their own compute.
Connection rate would count developers. Inference you didn't pay for would count
your own laptop. Every headline number on the dashboard would be
inflated by the people building the product, with no way to subtract them.
So development is excluded from every aggregate, and the billing rule gains one
clause: bill from usage where environment = 'production', and nowhere else.
The same user in two environments is two people#
You send the same identifier either way — soba.run({ user: "alice" }) is the same
line of code in your test and in production — so Soba files the person under the
environment of the key that sent it. alice under a development key and alice
under a production key are two end users, with their own machines, their own plan
assignment and their own place in the figures.
That is what makes the rule above hold for the person as well as the run. Without it,
testing against your own laptop as alice once would file her in development
permanently: her production runs would be billed while she herself sat on the unbilled
side of the ledger, and the laptop you paired during that test — a machine has no
environment of its own, it belongs to its owner — would appear in your production
machine list.
Pairing happens per environment
A machine paired by the development alice cannot serve a run addressed to the
production one, which is the same rule as everything else here: whoever the key says
you are is whose machine you reach.
Preview is not production-like#
Vercel's triad trained everyone to expect that it would be. It cannot be here, and
saying so plainly is better than borrowing a promise the architecture cannot keep.
Production's defining property is that the compute belongs to the end user. In
preview there are no end users with machines connected. Preview rehearses the
configuration (the routing, the ceilings, the plan shape), not the compute.
Preview is never a shared machine
One machine serving many developers' runs is operator pooling, the precise shape
the whole architecture exists to avoid. Preview is either
your app's own fallback under a cap, or a single service identity that owns its own machine. It is
never one person's Claude account serving the team.
A development key cannot reach a metered class#
TypeScript
development: { prefer: ["user-hardware", "user-subscription"],
allow: ["user-hardware", "user-subscription"] }
Not by discipline, by construction. The free tier is accident-proofed rather than
trusted: a loop in a test cannot bill you, because there is nothing billable in
allow. Exercising the metered path is what preview is for, and reaching one from a
development key is an explicit opt-in per key.
How the route resolves#
A key's environment sets a ceiling, and the user's plan narrows within it. Neither can
widen the other, and the machine then applies its own grant on top, unchanged. See
Security model.
| Environment |
Plan |
Result |
development |
none; you are not a paying user of your own app |
the environment's route alone |
preview |
none |
the environment's route, with a period cap required |
production |
the end user's plan |
the narrower of the two |
Two ceilings, not one#
TypeScript
maxCostMicros // ceiling on ONE run
periodCostCapMicros // ceiling on the calendar month
Every other ceiling in Soba is per run or per user. periodCostCapMicros is the
aggregate brake, and preview is where its absence bites first, because CI runs
unattended and nobody is watching a loop on a metered class. It is worth setting on
production keys too.
Your first development run#
The default machine grant is read-only tools inside
~/.soba/workspace, and that default is correct: it is what makes a worker safe to
point at a control plane nobody trusts. But it means testing an agent against your own project
has every file operation refused, in a directory Soba never granted: a first run that
fails looking like a broken product rather than a policy you need to widen.
The fix is not a weaker default. It is that you pairing your own machine to your own
development key is a different trust situation: the machine owner and the app owner
are the same person. So say so, in the file you control:
Terminal
soba-worker login --dev --root .
Pairs, writes a grant scoped to the current project rather than to the scratch
workspace, and prints what it granted. Deliberately explicit, deliberately
per-directory, and deliberately not the default for anything else.
The free tier that cannot run out#
A development key points at your own Claude Code or Codex. Your machine, your login,
your plan, and you are testing. Soba never touches the compute, so there is nothing for
it to bill and nothing for it to subsidise.
Credit-metered free tiers expire by design, and the moment they do you are evaluating a
bill rather than a product. This one has no meter to exhaust, because the thing being
consumed was already bought by the person consuming it.