The current manifest API is tinycloud.dev/v1alpha1. Tinycloud parses a strict YAML subset, validates it against tiny.v1alpha1.json, normalizes defaults, and then applies semantic and organization-policy rules.
Unknown fields are rejected. A typo is an error, not a silently ignored key.
Run pnpm tiny init [name] to write this shape and pnpm tiny validate to validate it locally.

Supported YAML

The parser accepts block and flow mappings/sequences, block strings, quoted and plain scalars, booleans, null, and numbers. It rejects anchors, aliases, custom tags, tabs used as indentation, and duplicate keys. Use one ordinary manifest document; YAML composition features are outside the supported subset.
These restrictions keep a manifest deterministic and safe to hash — which is what lets the normalized form identify a revision.

Top-level fields

metadata

string
required
A lowercase DNS-style slug, 3–63 characters. This is the app’s stable identity.
email
required
Normalization lowercases it and always adds the owner as an app admin.
string
Defaults to the slug.
string
map
Arbitrary key/value labels, e.g. department: platform.

runtime

Although the schema includes container and Python for future providers, the production Firecracker target accepts only TypeScript/JavaScript worker deployments. The local provider is an injected test fixture, not a deployable target or a CLI option.
Entrypoints must be relative, may not contain .., and must be included in the uploaded artifact.

build

build.command runs inside a throwaway Firecracker microVM whose only network is the package registry. The control plane, not the manifest, chooses the install command, and it is always the package manager’s reproducible mode (npm ci --ignore-scripts, pnpm install --frozen-lockfile, and so on) — a manifest cannot ask for a looser dependency resolution than the lockfile describes. build.output selects the directory that becomes the deployed artifact, and runtime.entrypoint is resolved inside it: with output: dist, an entrypoint of index.js means dist/index.js. Omitting output deploys the whole tree, including node_modules. lockfileRequired: true refuses the build when the matching lockfile is absent. Setting it to false is allowed, and tiny plan will warn that two deploys of the same source may install different code. Build output is re-packed through the same pipeline as an upload, so the per-file and total size limits apply, and credential-shaped files such as a stray .env are stripped rather than deployed. The result is content-addressed: an unchanged source, command, and toolchain reuses the previous build instead of running it again, and a rollback returns to identical bytes.
@tinycloud/runtime-sdk is supplied by the build image, so an application does not declare it as a dependency.
If the operator has installed no build image, the platform reports executesBuildCommands: false and tiny plan refuses a manifest with build.command. It never falls back to building on the control-plane host. Full guide: Builds.

access

access.visibility is private, organization, or public. requireLogin defaults to true; non-public apps may not disable it. Subjects have exactly one of user, group, or serviceAccount, plus an optional role (admin, editor, user, or viewer; default user).
Public apps are disabled by the default organization policy, so a public manifest is refused at plan time until an owner sets allowPublicApps.
Only the last row serves anonymous requests, and only a public app may set requireLogin: false. An anonymous request carries no identity token, so context.user is null — the handler is never given a synthetic user it might mistake for a real one. A visitor who does hold a session keeps their own identity, role, and bindings on a public app; anonymity is the fallback, not a downgrade applied to everyone. Only the production environment opens. A preview keeps the login redirect even when production serves the world, because visibility is read from the production revision and a preview is unreviewed work at a guessable hostname. Anonymous traffic is metered like any other request but is not written to the audit log one row per request: there is no identity to record, and a public app’s volume is not bounded by headcount. Denials, deploys, and every control-plane action are audited as before. Because that volume is unbounded, two organization-policy limits apply:
maxMonthlyRequestsPerApp is off by default so that an existing organization keeps one pooled budget. Set it when you make an app public: without it, a scraped public app can spend the whole organization’s monthly request quota and take the organization’s internal apps down with it.
Full guide: Access and sharing.

resources

Defaults are shown in the example.

database

Migrations are ordered .sql files. Once a migration has been applied, changing its contents is a deployment error; add a new migration instead. Migrations are read from the uploaded source, not from the build output, so a build.output that excludes them is fine. engine: sqlite is always available: one file per environment, held by the control plane and reachable only through the platform binding. It allows one open write transaction per environment at a time, which is what SQLite is. engine: postgres requires the operator to have configured a cluster. Each environment gets its own role and database, and the app’s SQL is the dialect Postgres speaks — use $1 placeholders rather than ?. Asking for an engine the control plane does not have is a plan-time refusal, never a silent substitution.
Neither engine gives the application a connection string. The database lives outside the replaceable microVM, and the guest reaches it by sending SQL to the control plane under its own workload token.

storage

resources.storage declares up to five object namespaces. Each has a name the app passes to context.storage(name) — the name may be omitted when only one is declared — and a maxSize quota. Objects are not mounted into the microVM: the guest sends each operation to the control plane over its private link under its workload token, exactly as it does for SQL, and the token decides which namespaces it may address. maxSize is measured against the namespace as it will be after the write, so replacing an object does not charge for the copy it replaces. Filling it is a RESOURCE_LIMIT_EXCEEDED refusal rather than a truncated write. Alongside it the platform bounds an object at 32 MiB, a key at 1024 bytes, and a listing at 1000 keys per call. Keys must be relative and may not contain ... retention describes how long deleted objects are kept for recovery. It is recorded on the claim and is not yet enforced by a reaper. Full guides: Databases · Object storage.

capabilities

Concise operations are qualified with the capability name during normalization, so payments.read becomes stripe.payments.read. A production capability request requires approval under the default policy. Every operation id has exactly three parts, because that is what the broker route addresses. See the capability catalog for every provider, operation, and enforced constraint, and Capabilities for the guide.

network

allowlist rules take a host and ports (ports default to [443]):
Direct egress is available only when the operator has enabled it on the worker; otherwise the manifest fails target compatibility with an explanation. When it is enabled, the host resolves each hostname once at deploy time and pins the addresses into that VM’s own firewall table, so the guest cannot widen its own allowlist afterwards by answering DNS differently. A hostname that resolves into a private range is refused outright.
Prefer a capability. An app with direct egress holds whatever credential that endpoint requires; an app with a capability holds nothing.
Full guide: Direct egress.

secrets

Names must be uppercase environment-style identifiers and cannot collide with reserved TINY_* names. The default policy allows raw secrets but makes them an approval signal. Prefer capabilities when an operation exists. Full guide: Secrets.

lifecycle

Durations use integer units such as 15s, 60m, 72h, or 30d, with tighter units enforced for individual fields.
  • previewTtl defaults to 72h and may not exceed the default organization maximum of 168 hours.
  • If both are set, archiveAfterUnused must be longer than sleepAfter.
  • deleteAfterArchived requires archiveAfterUnused.
Full guide: Lifecycle.

observability

Defaults are log level info, retention 7d, and trace sample rate 0.05. The manifest can also carry alert intent for error rate, latency, or request thresholds.
The current implementation persists structured worker logs and usage counters. It does not run a separate alert delivery service, so alert intent is recorded rather than acted on.

Target compatibility

Schema validity does not guarantee a target can deploy the manifest. plan compares the normalized manifest with the provider’s live capabilities and reports incompatibilities. The default organization policy caps memory at 2 GiB, which is also the Firecracker provider limit.
Declaring less than 512 MiB does not shrink the guest: the runtime raises any serving microVM to that floor.A Node guest below it cannot hold the runtime’s own text pages in its page cache and spends every scheduling slice re-reading them from the virtio disk, which stalls the app for tens of seconds. Firecracker faults guest memory in lazily, so a ceiling the app never touches costs nothing.
Check compatibility with either: