A worker entrypoint must default-export tiny.app(handler) from @tinycloud/runtime-sdk. The handler receives standard Web Request and Response objects plus a verified Tinycloud context.
Unhandled handler errors are converted to a JSON 500 APP_ERROR response and written to the deployment log.
The SDK is supplied by the runtime and the build image. An application does not declare it as a dependency.

Context

Identity is derived from a short-lived platform token verified by the runtime host.
Client-supplied Tiny-* headers are stripped at the gateway and are not exposed to app code. Never trust a header for identity — use context.user.

Database

context.db is not a connection. A microVM is replaceable, so the database lives outside it, and the app is given no host, path, or credential to leak. Statements travel over the VM’s private link to the control plane, carrying the deployment’s own workload token; the control plane resolves that token to exactly one environment’s database.
Available methods are query, get, execute, and transaction. A failed transaction rolls back automatically. Parameters are always bound, never interpolated, whichever engine is behind the binding. Write the placeholder syntax your engine uses: ? for SQLite, $1 for Postgres.

Value types over the wire

Rows reach your app as JSON over the private link, so values arrive as the JSON type that can hold them without losing anything. That sometimes means a string where you might expect something richer: Binary goes the other way as itself: pass a Buffer or Uint8Array as a parameter and it is bound as bytes, not as text.

Transactions are leased sessions

transaction opens a server-side session with a short lease rather than a lock the app holds. Two consequences are worth knowing:
  • Nesting is refused, not flattened. A flattened rollback would quietly commit work the caller believed was undone.
  • The lease expires. A transaction left open past the timeout is rolled back and its session reclaimed, so a crashed guest cannot hold a write lock or a pooled connection indefinitely. Keep transactions short, and never wait on a slow remote call inside one you need to commit.

Bounds

The platform bounds what one request may ask for: statement size, parameter count, result rows, and result bytes. A query that would return more than the row limit is refused rather than streamed — add a LIMIT and paginate. On SQLite, one write transaction is open per environment at a time; a second begin is refused with that explanation rather than blocking. Postgres serves several concurrently from its pool.

Object storage

Objects work the way SQL does: the guest holds no bucket, path, or credential, and every operation crosses to the control plane’s storage service over the private link under the deployment’s workload token. The token decides which namespaces the app may address, so a namespace another app declared is unreachable even by name.
The name is the one from resources.storage[].name, and may be omitted when the manifest declares exactly one namespace. Asking for a namespace the manifest never declared throws before any request is made.
  • put accepts a string or Uint8Array and returns the stored object’s metadata.
  • get returns Uint8Array | nullnull means that key is absent, while a missing namespace or a superseded deployment raises, so an unreachable namespace never reads as an empty one.
  • delete returns whether the key existed.
  • list returns objects sorted by key with key, size, lastModified, and etag, filtered by an optional prefix.
Keys must be relative and may not contain ..; anything that would escape the namespace is refused. Bytes cross the link raw rather than base64-encoded in JSON, so binary is stored exactly as given — the round trip is byte-for-byte, not UTF-8-normalized.
etag is a change validator derived from an object’s size and modification time, not a content digest. Two identical objects under different keys have different ETags. It is safe to compare an ETag with an earlier one for the same key to detect a change.
Bounds: an object is at most 32 MiB, a key at most 1024 bytes, and a listing returns at most 1000 keys per call. A namespace also has the maxSize its manifest declared, measured after the write, so replacing an object does not charge for the copy it replaces. Bytes moved are metered as storage_bytes.

Capabilities

The SDK sends the workload token and, when present, the user token to the control-plane broker. The broker validates the grant, user-presence rule, input schema, constraints, rate limit, and response filter before returning.
Vendor credentials never enter the worker.

Outbound fetch

context.fetch is for declared direct egress. It checks host and effective port against network.egress. On Firecracker, allowlist mode is deployable only when the operator has set TINY_FIRECRACKER_ALLOW_EGRESS=true; the host then pins each declared hostname’s resolved addresses into that VM’s firewall. Otherwise planning rejects the manifest. Brokered capabilities remain the preferred path because application code never receives the vendor credential.

Logging and audit

log writes a structured deployment log event at the manifest’s level. audit writes a structured app.audit event into the same log stream — use it for actions a person may later need to account for.

Secrets

Only secrets the manifest declared are resolvable.

Runtime limits

The runtime host enforces the manifest request timeout and concurrency.
A timed-out process is retired after its response, because JavaScript promises cannot be forcibly cancelled. Do not rely on a timeout to stop work already in flight.
The gateway applies the same default size limits.