The object model

tenant boundary
Owns apps, members, connections, secrets, policy, and audit history. Every organization-scoped read takes an organizationId and filters in the query, not afterwards. In API paths the literal self refers to the token’s own organization; an explicit ID from another organization returns 404.
unit of intent
Named by metadata.name, a lowercase DNS-style slug of 3–63 characters. An app has a lifecycle state — active, archived, or scheduled for deletion — that is a separate state machine from any individual deployment.
where an app runs
Every app has production. It may also have named environments and expiring preview environments. Durable resources bind per environment, which is why a rollout, a rollback, and a sleep/wake cycle all see the same data.
normalized manifest
Parsing resolves every default exactly once. The normalized form is what gets hashed, stored on the revision, and read by every downstream subsystem — so two manifests that mean the same thing hash the same.
content-addressed source
Remote clients upload bounded bytes before planning or deploying. The server never accepts a caller-selected filesystem path. Artifacts omit .git, node_modules, .tiny, common caches, and credential-looking files.
one immutable rollout
A deployment walks an explicit state machine: provision resources, apply migrations, boot a candidate microVM, health-check it, and only then switch the route. Retryable failures retain their current state for idempotent reconciliation.

Deploy is asynchronous

POST /v1/apps:deploy persists desired state plus a durable, leased job and returns 202. A background reconciler does the work and resumes those jobs after a crash.
A deploy that returned is not a deploy that happened. tiny deploy waits for a terminal state by default and exits non-zero if the deployment failed. Use --no-wait only when a script genuinely wants fire-and-forget.

Four identity layers

Each layer is a separate audience, and every token carries one. A token minted for one boundary is rejected at another. The workload token is the load-bearing one. It is what lets a replaceable microVM have durable state without holding a credential: the control plane resolves the token to exactly one environment’s binding, and a superseded deployment’s token stops working the moment the route moves.
Client-supplied Tiny-* headers are stripped at the gateway. Application code can never see a forged platform header.

Two ways an app reaches the outside world

Capabilities (preferred)

The app calls a named operation. The broker validates the grant, constraints, rate limit, and response filter, injects the credential into a header the app cannot set or read, and returns only status and payload. The app holds nothing.

Direct egress (opt-in)

context.fetch against a manifest allowlist. Off unless an operator sets TINY_FIRECRACKER_ALLOW_EGRESS=true. An app with direct egress holds whatever credential that endpoint requires.

Approval gates on blast radius, not on deploys

Redeploying an app whose permissions did not change is fully automatic — that is what makes the platform usable by an agent. Approval is required only when a change widens blast radius in production: a new capability, a new raw secret, a new outbound host, widened visibility, or a destructive migration. A gated deployment sits in awaiting_approval — a pause, not a failure. Approving resumes the same deployment. See Approvals and ADR 0004.

Where things run

Linux with /dev/kvm. Runs the API, the embedded gateway, the reconciler, the capability broker, the data service, and the object store. Holds the SQLite control-plane database, artifacts, app databases, and storage namespaces under $TINY_DATA_DIR.