Deploy an application
Prompt:
Deploy this application to Turn Zero Cloud.
Also works:
- "Ship it."
- "Put this online."
What your tool does
- Asks you which plan the application starts on,
free,standard, orpro, before creating anything. The platform assigns no default plan. - Reads the project for the application's name, its server entry point, its health path, the services and library packages it uses, and the hosts it calls.
- Stores each key the code sends to an external application programming interface (API) through the line
store_secretreturns, never as a tool argument. Supply the value without exposing it describes the line. - Calls
declare_upstreamfor each API the code calls on a stored key. Code that calls through a provider's software development kit (SDK) stays unchanged: the declaration lists the settings (environment variables) the SDK reads, and each deploy and promote injects them (An unchanged SDK). Other code it changes to call/egress/v0/<upstream>/<path>through the egress gateway. An upstream's key never becomes an environment variable of the deployed application, while a value the manifest'ssettingsbind does. - Declares in the manifest's
egressmember each host the application's own process dials, as exact lowercase hostnames or*.example.comfamilies. An upstream reached through the gateway needs no entry, and neither do the platform's own endpoints. - Checks the backend against the Node.js Runtime package's rules. The backend runs one server honoring
PORTand calls external hosts with the runtime's globalfetch, undici, or an SDK built on them. Each handler ends its response whenrequest.signalaborts, and no work runs after a response. - Responds on the manifest's health path from the moment the server listens, with 503 until the application is ready, never a 4xx, and then exactly 200. A server with a database can wait to listen until its migrations finish. The health check gives the check's figures.
- Calls
create_applicationwith the name and the plan, and keeps the application identifier from the response for every later call. - Writes
manifest.jsonwhere the project has none, and callssubmit_manifestwith the whole document. Through the Model Context Protocol (MCP) tool, the response withholds the development credentials it mints and statescredentials: "withheld". - Where the manifest declares a database, reads the plan's
database-connection-limitwithread_plan_quotasbefore writing the client pool, or takesconnection_limitfrom thesubmit_manifestresponse. The pool takes its maximum fromAPP_DATABASE_CONNECTION_LIMIT, the same number, which each deploy and promote injects. - Before the first deploy, runs the application on your machine as Run locally describes and requests its health path.
- Builds the application's folder on your machine.
- Calls
deploywith the application identifier, naming no environment and neitherartifactnorupload. The response is status 200 withstate: "awaiting_command", the line ascommandand its Windows form ascommand_windows, andnext, theread_statuscall to make after the line. - Runs that line for its operating system once, as given, from the application's folder. The line runs the turnzero-cloud command with a one-time deploy code, so no long-lived token reaches the shell. The command zips the folder, uploads the zip, and starts its deploy. It then waits for that deploy and prints each step and the outcome, so your tool allows it several minutes or sets
TURNZERO_DEPLOY_NO_WAITto have it return after the start. - Where the command returned before the outcome, makes the
nextcall,read_statusfor the environment deployed withwait_seconds45, which waits to respond while the deploy runs. Where the command printed a refused start, it clears the cause and callsdeploynaming the upload, as step 3 describes. - Makes that
read_statuscall until the environment'sdeploy.stateleavesdeploying. Where the state isfailed, it reads the record'soutcomebefore retrying. For a failed health check, it readsoutcome.gate, thenread_logswith the sourcecontainerfor more console lines. - On a new application, checks production as Check the release describes.
- Where you turned development on, the deploy goes there. Before asking, your tool requests each route the change touched on the development hostname and checks
read_logsfor errors, fixing the code and deploying again where one fails. - Then asks you whether to promote once the development deploy is
deployed. On your yes, it callspromotewith the application identifier andwait_seconds, a step of thereleaseskill, so production runs the version the development environment ran. The promote's response waits and ends as the deploy's does. Where it returns 202, the tool follows itsnextcall until production'sdeploy.stateleavesdeploying. - Only to run the application on your own machine, calls
submit_manifestnaminglocal_run: true, and runs the response'sprovisioning.commandonce, as given, in the application's folder. The provision line writes.envand prints no credential value. A hosted deploy needs nothing from it. - Reports the hostname the version runs at, the version number, and the files it wrote, and never commits
.env. - Asks for no browser approval:
deploy,promote, andcreate_environmentrun without one. Rolling back, halting, renaming, and deleting are covered in Manage versions and environments.
What you need
- Node.js 24 or later, and npm, installed on your computer for the local build. They also run the turnzero-cloud command through
npx. See What your computer needs. - The plan the application starts on. Your account can have only one live Free application at a time, and during the beta only one live Standard and one live Pro application.
Before your AI starts
This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.
- Your tool connected and signed in (Connect your tool).
- A backend written as ordinary Node.js code that exposes one server honoring
PORTand responds on a health path with status 200. The Node.js Runtime package states the rules. .envlisted in your ignore file, because the provision line writes credentials into it.- Where the application declares a database, prove its migrations and queries locally first, without using any of the day's deploys (Test your application locally).
Steps
A first release takes the five numbered steps below. A new application has one environment, production, so your tool deploys the artifact straight to production, with nothing to promote. The deploy skill contains the hosted steps on its first page and the local-run steps on a page of their own. This page explains each step so you can follow what your tool reports.
The sections after step 5 cover turning on a development environment, a local run, the deployed copy, deploying a new version, and an existing application. Manage versions and environments covers rolling back, the version history, halting, renaming, and the two deletions.
1. Create the application
create_application requires name and plan. The name has 1–40 characters: a lowercase letter first, then lowercase letters, digits, and hyphens. The plan is free, standard, or pro. Your tool asks which plan you want, because the platform assigns no default.
The response's application member contains the identifier id, which every later call uses. It also contains the name, the label, the creation time, and the plan. The label combines the name with a unique key, and both hostnames under ai.host come from it:
<label>.ai.hostfor production;<label>-dev.ai.hostfor development, once you turn it on.
A hostname retired by a deletion or a rename is never used again.
2. Declare the manifest
submit_manifest takes the application identifier and the whole manifest: the services the application uses, its health path, its region, the hosts it may reach, its audience, and its library entries. An invited or workforce audience gates the health path from outside unless session_free_paths covers it, but the health check reaches it directly, so deploys need nothing listed. The manifest explains each member, and Author the manifest shows how to write one.
The first submission mints the development platform credential. The submission that first declares the database kind also mints the development database role's password. Where the values go depends on how the call arrived:
- Through the MCP tool. The response withholds the values and states
credentials: "withheld", so no platform-minted credential value enters your assistant's context. It also includesprovisioning. Withlocal_run: truein the request, it hasfor,command,command_windows, andexpires_at, which are the provision line and its expiry. With nolocal_run, it hasforandnext, which says to submit again naminglocal_run: true, and the call creates no grant. - Through the Hypertext Transfer Protocol (HTTP) API. Without
local_run, a script of your own receives the values ascredentialandplatform_credential, the response statescredentials: "answered", and it has noprovisioning. Withlocal_run, it returns the line and withholds both values, a first submission included. Itsprovisioning.nextsays the line writes both.
The response's other members:
development_databasestates the development database's connection facts on either surface.databasestates the production database's connection facts once it exists. Its credential is never returned.connection_limit, the client pool's maximum, appears wherever the manifest declares the database kind.health, on every response, gives the path the health check requests (path), the one status that passes it (passes_on, 200), and its bound in seconds (waits_seconds).
A later submission that finds both development credentials in place mints neither. One that completes a partial database provisioning returns the database credential once, through the HTTP API alone.
A manifest that fails validation is refused 400 manifest_invalid, and the refusal's violations member lists the JSON path of every violation. The region is usa, the one region offered at the beta. The schema's other three regions are refused 400 region_unavailable.
What a submission provisions
The response's detail has a line for each service set up, one for names not stored yet, and a last line pointing here:
- End-user realms. The first submission declaring the accounts kind creates a realm for each environment: production's alone on one environment, where local runs sign in.
realmnames production's, and development's is that name plus:development. - The realm member. The manifest's
realmmember sets up each environment's realm, andconfigure_realmrefuses to change a field it sets. - Push. The providers the manifest lists are set up on each environment. With no provider named, store each credential with
store_secret, then callconfigure_push. - Database. The first submission declaring the database kind creates the development database and role, for local runs. Production's comes with its first deploy, or the first promote on two environments. A later submission keeps the development credential;
rotate_secretreplaces it. - Schedules. A schedule runs only after a deploy made at or after its declaration, which on two environments is a promote for production. The line names each environment still needing one.
- Issue tracking. One space covers every environment, and the application reaches it through the egress gateway. With a space for each environment, production's comes at the first promote.
- Not stored yet. The line lists each name a setting, an upstream, a sign-in method, or a push provider uses that has no stored value, and the environment where only one lacks it. It checks only the application's environments, an upstream's key at development too. Store each with
store_secret. Until then, a deploy is refusedsetting_secret_missingfor a setting, and the gateway refuses an upstream's callscredential_not_in_custody. A sign-in credential takes effect at the next submission after its store.
Where the detail would pass its length, lines whose facts a member or a read gives are left out in a fixed order, the pointer last. Lines on the database's connection limit, push, deploys owed, ended upstream keys, names not stored yet, values returned once, and local runs always stay.
For a run on your own machine, submit again naming local_run: true, and follow Run locally.
3. Build and upload the artifact
Your tool builds the application on your machine into one folder, which the turnzero-cloud command zips. The zip contains:
- the backend as ordinary Node.js code exposing one server that honors
PORT; - the web client's built static assets, where there is one;
- the
migrations/folder, where the application has a database; - the
lib/folder with the library packages the application declares asfile:dependencies; manifest.json, the project's copy, optional, which no deploy runs under, because the platform keeps the onesubmit_manifestrecorded. A copy that differs getsmanifest_notice, a warning that the deploy runs under the recorded manifest, listing the members that differ. The notice also lists eachlib/<name>/package.jsonwhoseturnzero.entryandversionmatch no entry of the recordedpackages, and each the deploy left unread or could not compare, even withoutmanifest.jsonin the zip. Amanifest.jsoncopy larger than 1 MiB or nested deeper than 64 levels is refused (below);- the package manifests,
package.jsonand its lockfile.
A file the list does not name, such as a test/ folder, is still deployed: the image build copies it into the image unchanged, and it runs only where your code loads it.
The zip's root is the folder containing package.json and its lockfile. Where there is anything to install, the image build runs npm install --omit=dev there and links the copied library packages among the dependencies. It then runs npm start on Node.js 24. So the start script is the entry point. node_modules is not in the zip, and an environment file never is.
The project's library folder, system/, stays out of the zip. The zip includes the application's own copy of each package under lib/ instead, which package.json declares as a file: dependency (Use the library). Where the application sits in app/ beside system/, run the command's lines in app/, so the zip contains the contents of app/ alone.
Build a database-backed service lays out such a project, shown here without the contents of system/. The zip contains the contents of app/ and leaves out node_modules and the .env file the provision line writes for a local run. app/ is the zip's root, containing package.json and its lockfile, manifest.json, and src/main.mjs, the entry its start script runs:
notes-service/
system/ the library entries as taken, never zipped
app/ the zip's root
package.json
package-lock.json
manifest.json
.gitignore
AGENTS.md
migrations/0001_notes.sql
src/db.mjs
src/server.mjs
src/main.mjs
test/notes.test.mjs
lib/database/ the package's package.json and lib/, copied from system/
The command builds its own zip without what What it reads lists. hash and deploy print paths left out. They warn of a .zip file at the folder's top, which the zip includes (keep a build's zip elsewhere), and of a file that looks like a key or a credential. hash --list lists each file. A zip you build yourself, such as one for the artifact form in step 4, follows the rules below.
On Windows, build such a zip with tar.exe, which Windows 10 and 11 include. Run tar.exe -a -c -f app.zip --exclude node_modules --exclude .env --exclude '.env.*' --exclude .git -C app . in Windows PowerShell from the folder containing app/. It writes app.zip there, with / between the segments of each entry's name. It leaves out every node_modules folder, .env and each .env.* file such as .env.local, and every .git folder. Windows PowerShell's Compress-Archive writes \ between the segments instead, which the deploy refuses.
Every entry of the zip is a relative path inside its root, with / between its segments, and a folder entry such as zip -r writes becomes a folder. A deploy refuses an entry that is absolute, contains a backslash or a NUL, names a drive, or leaves the root through .., since the image build cannot write it.
A deploy refuses a zip naming one path as both a file and a folder the same way, and a name longer than 4,096 bytes or deeper than 128 segments. Each is refused artifact_layout_invalid, with a detail naming the entry, and for a backslash the tar.exe command above. So is a zip of more than 65,535 entries, 500,000 name segments in all, or 128 MiB declared, its folder entries counted, and one whose root manifest.json nests deeper than 64 levels.
A deploy reads the zip before adding a version, so none of these refusals counts a deploy. Bytes that are not a zip are refused artifact_unreadable. A zip with no package.json at its root, or whose package.json is not a JSON object, is refused artifact_layout_invalid. So is one with neither a start script nor a root server.js, since npm start then has nothing to run, and one whose root package.json or manifest.json is larger than 1 MiB. Where exactly one top-level folder of the zip contains a package.json, the detail names that folder: zip its contents instead.
A zip whose root package.json or manifest.json, or a nested package.json the deploy reads, inflates past the size its entry declares is refused artifact_unreadable too. The image build stops at any entry that does, naming it.
The install runs your runtime dependencies alone. The image build removes the install scripts, such as postinstall and prepare, from your root package.json and from each workspace member's, one whose folder your root package.json's workspaces match. The image contains those copies. Every other dependency runs its own install scripts as npm runs them, a lib/ package declared as a file: dependency among them.
A workspaces pattern may use literal segments, * within a segment, and ** segments, with each ! pattern after every other and none matching a positive pattern's own text. A * never matches a folder name's leading ., and a ** never passes through a folder whose name opens with ., so packages/* and ** skip .cache. A segment that spells the dot itself, such as .c*, still matches it.
A deploy refuses any other pattern by name, such as one using ?, braces, brackets, parentheses, a backslash, a leading #, or a .. segment. It also refuses a pattern longer than 4,096 bytes or deeper than 128 segments, the bound on an entry's name, and more than 256 patterns or 5,000,000 matching steps.
No binding.gyp of the root's or of a workspace member's is built either, so such a native module ships prebuilt. A deploy refuses each one that ships no .node file, as artifact_layout_invalid, naming the entry. The root's ships its .node anywhere in the zip outside node_modules and the members' folders. A member's ships it in its own folder, less any member nested inside it. A lib/ package's binding.gyp builds on the platform's Linux builders, as npm builds it.
Build each .node file your zip ships for Linux, the platform's target. The deploy checks only that it exists; one built for Windows or macOS fails when your application loads it.
Development dependencies are never installed, whatever your .npmrc says. Each dependency the install fetches, from a registry, a git repository, or a tarball, runs its own install scripts as npm runs them. Your .npmrc still reaches the install for everything else, such as a private registry, except the shell those dependency scripts run under, which the build sets.
A deploy returns artifact_notice, listing each install script and binding.gyp of the root's or a workspace member's that the build will not run or rebuild. The deploy reads a package.json below the root only up to 64 KiB, and at most 50 of them. The notice lists each workspace member's it left unread, which the build strips all the same. Where one builds something your application needs, run it locally and ship its output in the zip.
The image build skips npm install where it would install and run nothing. That is where package.json declares no dependencies other than devDependencies, and no workspaces. The skip also needs a zip root containing no node_modules or .npmrc, and a package.json with no os, cpu, libc, or devEngines field, which npm's install checks. A skipped install changes nothing else: the image still starts with npm start.
The application reaches the platform in one call and one line:
- Your tool calls
deploywith the application identifier, naming no environment and neitherartifactnorupload. The response is status 200 withstate: "awaiting_command", the line ascommand, its Windows form ascommand_windows,expires_at,previous_code, andnext. It adds no version and counts no deploy against the plan's daily quota. A production deploy's call can also namerotate_database_credential: true, which step 4 describes. - It runs the line once, as given, from the application's folder, on Node.js 24 or later:
commandon macOS and Linux,command_windowson Windows. The command zips the folder, prepares the upload under the line's code, uploads the zip through the HTTP storage interface, outside MCP, and starts its deploy. It then waits for that deploy, as Waiting for the deploy describes. Where the command returned before the outcome, your tool makes the response'snextcall,read_statuswithwait_seconds, which step 5 describes.
There are two ways to deploy with the command:
| Who deploys | What runs | Credential |
|---|---|---|
| Your AI tool | The line its own deploy call returned, once. |
The line's one-time deploy code. |
| A job with no AI tool, such as a continuous integration (CI) job | The unattended line Upload samples shows, with no code piped. | TURNZERO_CLOUD_MINTED_TOKEN, and TURNZERO_CLOUD_ORIGIN beside it off the default origin. |
The code is a credential for one use, for that one application, until expires_at, five minutes by default. It reaches the command on its input, never in its arguments. The command sends it once, on the call that prepares the upload, which spends it. It then uploads, starts, and waits under the short-lived upload grant that call returns, which no response shows your tool.
Other programs can read the line: the shell's process list, and anything recording the terminal. So the line is for one run by the tool that asked for it. Paste it nowhere, and do not put it into a pipeline's log.
A new deploy call ends the previous code and any upload it prepared that has not started, so your tool does not call it again while its line still uploads. The response's previous_code says what became of the last code. Its state is none, not_used, expired, replaced, withdrawn, or used. A used code also names its upload and whether it started.
A line whose code is unknown, expired, or already used is refused 403 deploy_code_refused. Call deploy again; where previous_code names a started upload whose id no prepared: line of yours printed, roll back, rotate the application's secrets and its database credential, and report it. list_versions and read_status show a deploy you did not start.
The line is returned in two forms, and the Windows form, with npx.cmd for npx, runs in every Windows shell. Windows PowerShell gives the reason.
Where the application's folder, or a zip you built, is elsewhere, or your tool's shell may run the line from another folder, the call names that path as local_path. The line then has --path, and the platform never reads it.
A path with a control character, a double quote, $, a backtick, %, !, &, |, <, >, ^, a typographic double quote, a doubled backslash, or a trailing backslash is refused 400 invalid_request. A shell would change such a path in the line, so run the line from that folder instead. The line does not expand ~: name your home folder in full.
A quoted path ends in no backslash. In cmd and Windows PowerShell 5.1, a backslash before the closing quote keeps the quote in the path.
The upload grant's one write that lands spends it for uploads, and the deploy's one start spends it for starts. It expires five minutes after the preparing call by default. After the start, it allows only the command's own progress reads of the deploy it started. They are accepted until five minutes after that deploy ends, never past fifteen minutes after the start, whatever the grant's expiry.
Only the command's grant can write to the deploy area. Your account's own credential, your tool's connection, and a minted token cannot, and are refused area_scope_refused where the request names its environment, so your tool runs the line as the response gives it.
The file lands in your application's deploy area, deploy-<application id>, which the platform creates and keeps. It keeps one pending upload per environment, and a later line's upload replaces one no deploy has read. The file is deleted once the deploy that read it ends. Store files describes the area.
Under the token, the command makes the preparing call itself and uploads under the grant it returns, so the token never reaches the upload or the start. Deploying unattended gives the rules, and Mint a token describes the token.
The artifact form is the route that needs no turnzero-cloud command. A script of your own uploads the zip into an area of your own and names the file in artifact (step 4). It declares the area with declare_storage_area, bound to the application. It uploads with one of the two commands that mint_upload_grant returns, which need no turnzero-cloud command, or with that response's put line. An upload grant from your tool describes both. Or it uploads under a minted token (Mint a token). An upload under a minted token sends two headers:
x-turnzero-cloud-environmentgives the environment whose partition receives the file: the environment of the deploy that follows,productionon a new application. An upload under a session or a minted token without it is refused 400environment_required.X-Acting-Identitygives the acting identity: an application-chosen string distinct from the credential. Every storage write sends it, and an upload without it is refused 400identity_required.
Store files describes the storage interface, the upload grant, the two headers, and an artifacts area of your own.
Web client
A web client's built files are the zip's client/index.html, client/app.js, and client/styles.css. The platform serves those three itself: a GET of /, /index.html, /app.js, or /styles.css returns them ahead of your routes, and reaches your application only where the version lacks the file. Any other method, path, or file under client/, such as an image, reaches your application.
A web client is optional. An application without one, such as the backend of a mobile app, ships no client/ folder, and every request reaches its own routes. Build a mobile app's backend walks through that shape.
Upload samples
The line a deploy call returns follows, with the code in place of <code>. It is for the application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13, and the platform chooses the environment. A test checks it against the form the platform builds:
echo <code> | npx -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz deploy --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13
That is command. command_windows is the same line with npx.cmd for npx. The address gives the version of the command that runs, and the platform's response gives the version it currently offers. What it prints lists the lines the command prints.
The unattended line for the same application follows, in its macOS and Linux form. A job runs it from the application's folder with TURNZERO_CLOUD_MINTED_TOKEN set and no code piped. A test holds it to the form the command reads:
npx -y https://turnzero.ai/packages/turnzero-cloud-0.8.0.tgz deploy --application 3f6c2a1e-8d4b-4c7a-9e21-5b0f7d9c4a13
An upload without the command, under a minted token, is shown in Store files.
Waiting for the deploy
After the start responds, the command waits for the deploy it started and prints each step and then the outcome as they arrive. The turnzero-cloud command gives the lines it prints, its bounds, the statuses it ends with, and TURNZERO_DEPLOY_NO_WAIT, which has it return after the start.
Where your shell lost the command's output, read the deploy with read_status instead of running the line again. A second run is refused deploy_code_refused, since the first run spent the code, and it ends the first run's upload where that upload has not started.
When the start is refused
Where the upload lands and the start is refused, the command prints the refusal, and nothing is deployed. A refusal from the deploy's own checks leaves the upload grant allowing no further start. Where the cause lies outside the zip, such as the daily quota, clear it and make the retry call the refusal gives. It starts the uploaded zip again with no new upload, with the commit the start named. Where the zip itself was refused, fix the folder, call deploy again, and run its new line.
4. Deploy
deploy takes the application identifier and, optionally, the environment. A call giving none deploys to production on an application with one environment, and to development on one with two. A call naming no source returns the line of step 3. The two sources:
upload, the id the preparing call returned, which the command'sprepared:line prints. The command's start sends it. Send it yourself only to restart an upload whose start was refused or never made. The platform reads the file the command wrote and hashes it itself. Where that hash is not the one the preparing call named, the start is refused 409upload_hash_mismatch.artifact, a reference to a file in an area of your own: the storage area, the file name, and the SHA-256 hash. This form needs no turnzero-cloud command. Its optionalenvironmentmember gives the partition containing the file, not the environment deployed, and defaults to that environment.
A call naming both is refused 400 invalid_request, and so is one naming local_path beside either. The start may also name commit, the commit the code was built from. The command's start sends it by itself where the folder is inside a Git repository and the commit can be read, and sends none otherwise. Issue Tracking says what the platform keeps and records.
Add wait_seconds, a whole number from 1 to 45, to have the call wait to respond until the deploy ends. The command's start sends none and returns at once, and the command's own wait then follows the deploy to its end. Where the command returned before the outcome, the next of the call that returned the line applies instead. A value outside that range is refused 400 invalid_request before anything is written.
The deploy checks the request before writing anything, and returns the first check that fails. The checks run in this order:
- the application;
- the manifest;
- the environment;
- a deletion in flight;
- the artifact's area, presence, and hash, or the upload's file;
- the zip itself: bytes that read as a zip, and a layout that can run;
- a deploy in flight, then, where the environment serves no version, the health path named in a source file, refused 400
health_path_unservedotherwise; - each setting the manifest binds, its secret stored at the scope of the environment deployed, refused 409
setting_secret_missingotherwise; - where the application has a database, the plan's
database-connection-limit, refused 409plan_quantity_unsetwhile it is not set, so a retry of the deploy in flight returns that deploy; - the plan's daily deploy quota;
- for a deploy to development, room for it in the hosting cell's development group.
A halted development environment resumes only after every check passes, so a refused deploy leaves it halted. Among the refusals:
- On one environment,
developmentis refused 409environment_not_created. On two,productionis refused 409environment_unavailable, because production receives a version throughpromote. - A deploy to a halted production is refused 409
target_environment_halted. - An unreadable zip, or one whose layout step 3 refuses, is refused 400
artifact_unreadableorartifact_layout_invalid. To an environment serving no version, a zip naming its health path in no source file is refused 400health_path_unserved. None adds a version or counts a deploy. - An upload with no file is refused 404
upload_not_found: not uploaded yet, expired unused, replaced by a later line's upload, or already read by a deploy that has ended. - Under the upload's grant, a call the grant does not allow is refused 403
transfer_grant_not_admitted. The start is refused 403transfer_grant_expiredafter the grant's expiry, 404upload_not_foundbefore its upload lands, and 403transfer_grant_spentafter its one start, naming its deploy where one did. These spend nothing, nor does a malformed call'sinvalid_requestor a deletion in flight'sdeletion_in_progress. A refusal from the other checks above spends the grant. - A second deploy with
artifactwhile one is in flight returns the deploy in flight when its artifact hash is the same. It is refused 409deploy_in_flightwhen the hash differs, and whatever the hash while a platform redeploy of the environment is in flight. - A second deploy with
uploadreturns the deploy in flight only where that deploy read the same upload. Any other upload is refused 409deploy_in_flight, even one whose bytes are identical. - A deploy past the plan's daily quota is refused 429
deploys_per_day_exceeded.
The Refusals table below lists the rest.
The quota is the plan's deploys-per-day quantity, counted over the application's deploys started within the last 24 hours. The next deploy is accepted once the oldest of them is more than 24 hours old. A promote is not a deploy and does not count. A deploy to a halted development environment ends the halt (Halt and resume).
The deploy is asynchronous. Once its checks pass, the call records the deploy. Without wait_seconds, it returns at once with status 202. The body contains summary, one sentence on the environment, then application, environment, version (the next number in the application's history), state: "deploying", hostname, and health_path, the path the check probes. Its detail describes the wait.
With wait_seconds, the call waits to respond until the deploy ends or the wait runs out, counted from when the call arrived:
- Where the deploy ended, the response is status 200 with
statedeployedorfailed,settled: true, and the record'soutcome. - Otherwise it is status 202 with
settled: falseandnext, the exactread_statuscall naming the environment deployed, such asenvironment: "production", andwait_seconds: 45.
Both include waited_ms. The wait takes the application's one waiting place, so a waiting read_status for the same application returns at once. Its settled still says whether anything was in flight when it checked. A deploy that finds the place taken returns at once, as if its wait ran out.
A response that is not the platform's own, such as a gateway's error page or a closed connection, says nothing about whether the deploy started. Call read_status with wait_seconds first, and deploy again only where it shows no deploy started after your call.
The platform continues the work after the call records it:
- It builds the image.
- It starts the new version beside the running one, where one runs. The new copy receives the environment's database connection string and connection limit, its platform credential, and
TURNZERO_CLOUD_REALM_KEYSwhere the environment has a realm. Settings and environment variables a deployed copy receives lists every setting. - It publishes the client assets and polls the new copy's health path.
- When the health path returns status 200, it switches the environment to the new copy. A first deploy records
deployedat once. Where a version already ran, the switch waits until the deploy is 30 seconds old, anddeployedcomes 3 seconds after it, the status showingswitchingmeanwhile. - It removes the previous copy about a minute after the switch.
A deploy to production applies its image as a promote does: a new production platform credential each time, and the production database at the first. The call returning the line also takes rotate_database_credential: true, which sets a new production database password. The code records the value and the deploy applies it alone, so a preparing call or a start naming another value is refused 400 invalid_request. Where a start is refused, the retry the platform gives includes the value too. A development deploy refuses the member.
A deploy to a development pod typically takes up to about two and a half minutes, and one to its own container app up to about three and a half. Most of it is the image build, up to about two and a quarter minutes for an application's first version and up to about a minute and a half for a later one. Each version row's timings give your own figures. While the work runs, the status gives the step it is in, such as image_build, health_gate, or switching.
The health check
The check sends GET to the manifest's health path on the new copy every three seconds, for up to 180 seconds. It passes only when the path returns status 200 exactly. The check follows no redirect, so the health path must return 200 itself. A 3xx response counts as a failed probe and never ends the check early, so a path that only redirects fails at the 180-second bound.
Each probe waits at most ten seconds for the response to begin. Near the end of the check it waits only for the time left, but never less than one second. So a health handler must begin its response within ten seconds, and a probe waiting longer is recorded as timed_out.
A stable client error ends the check early. When twenty probes in a row return the same 4xx status, other than 408, 425, or 429, the platform checks whether your own process is responding. It sends one control probe, a request without the router mark, right after the twentieth. Where your process responds to it, the check ends early, about a minute in, and the record's outcome.gate contains ended_early: true.
Any other response during the twenty probes starts the count again, and a 200 passes the check. A 5xx response or a transport error, such as a failed or timed-out connection, never counts toward the twenty. A process that is still starting returns them. Nor does a 4xx from something in front of your process end the check early.
Respond on the health path from the moment the server listens: 503 until the application is ready, never a 4xx, then exactly 200. A server with a database can wait to listen until its migrations finish, as the Database package asks: until it listens, the check treats it as starting and keeps waiting. A route mounted after the server starts listening returns 404 until it is mounted, and twenty of those end the check early.
A first deploy whose zip names the path in no source file is refused health_path_unserved before any build. Author the manifest says how to choose the path.
Where the manifest declares the database kind, have the health route perform one read over the connection and return 503 while that read fails, as the deploy skill asks. The Database package's integration guide describes what the read proves: a pass shows the connection setting reaches the database, not only that the process responds. Build a database-backed service shows such a route.
Return that 503 with a fixed body, such as {"ok":false}, with no error text, credential, or host. Where the check fails, the deploy's record keeps the first 512 bytes of the last non-200 body your own process returned, and list_versions returns it too.
Where the deploy's response comes before the deploy ends, follow it to its end with read_status and wait_seconds, described in the next step. A deploy into a full development group is refused 409 group_full.
A failed deploy ends its record in the state failed, with the failure's name and detail in the record's outcome. A failed deploy or promote also gives the step it stopped at in step, such as image_build, compute_apply, or health_gate. Where the health check failed (health_gate_failed), the outcome also includes gate, the check's evidence, collected before the platform removes the new copy:
| Member | What it contains |
|---|---|
probe_sequence |
The probe responses as runs of an HTTP status or a connection word, the last sixteen runs. |
last |
The last non-200 response's status and content type, with the first 512 bytes of its body where your own process responded. Null where no probe got an HTTP response. |
answered_by |
application where your process responded to the control probe with the harness's router_mark_required refusal. intermediary where something in front of your process responded, nothing where nothing responded, and unknown otherwise. |
compute |
The new copy's state and restart count, or unavailable with a cause word where the platform could not read it. |
console |
The last forty lines of the new copy's console, at most 4,096 bytes, with truncated where the limit cut them. |
ended_early |
True where the check ended early on a stable client error from your own process, and false where it ran to its bound. |
Each control probe may leave one harness_refusal line in your console. For a development pod, answered_by is usually unknown, because the pod receives no request until it is ready. The record's detail sums up the evidence and gives no address. Where the evidence shows a likely cause, such as no route at the health path or a process that stops, the detail opens with it and its remedy. Where the last response was a redirect, the detail says so and that the check does not follow it. list_versions returns the same outcome.
For an environment running as a container, console lines reach the log store after a delay. The platform waits up to 60 seconds for a first line before recording the tail. Where none arrived, console is empty and says so. Where a version is running, the lines are then readable through read_logs. After a failed first deploy they are not: read_logs answers only the lines the check kept.
Other failures:
- A deploy interrupted by a platform restart before its switch ends
failedwith the outcomeinterruptedwithin about fifteen minutes. A retry with the same artifact completes the work. With the upload form, a new call and line of step 3 upload the zip again, because the file is deleted when its deploy ends. A deploy interrupted after its switch endsdeployed, because the new version is already running. - When the environment already runs a version, a failed deploy changes nothing the platform manages. The running version, its settings, and its credentials keep serving, and the new copy is removed.
- A first deploy whose check fails removes the container it created. The retry creates it again and completes any provisioning step left unfinished.
- A deploy fails with the outcome
slot_busywhen it cannot remove the previous container in time, or when that container is still present at the end of its wait. Deploy again.
The platform neither reverses nor prevents what the new copy's own code did while it ran, such as a migration at start; Add a database says how to write one. Read the failure before retrying.
When the image build fails
A deploy whose image build fails ends failed with the outcome image_build_failed and the step image_build. Nothing is applied, and a running version keeps serving. The outcome's build member contains the build's own last lines, most often the output of npm install:
| Member | What it contains |
|---|---|
outcome |
lines, empty where no line was kept, or unavailable where the platform could not read the build's log. |
output_tail |
The build's last forty lines, each at most 512 bytes and 4,096 bytes in all. |
truncated |
True where a limit cut the lines. |
cause |
Present where the log was not read: log_unavailable, or log_timeout where the read ran out of time. |
The lines leave out the build service's own lines and every line mentioning a container registry, a header value, or the hosting provider. Any other address appears as [address], and a credential as [token]. The deploy failed event that read_logs returns contains the same lines.
Fix the artifact, such as its package.json or lockfile, and deploy again. A build the platform could not start ends deploy_failed instead, with no build member.
5. Read the status
The read_status call takes the application identifier and an optional environment, development or production. Its top-level members describe the environment it names, and production where it names none. After a deploy or a promote, call it until deploy.state leaves deploying. The action's next call gives the environment it acted on. It returns no separate health result. A failed health check's evidence is the gate member of the deploy record's outcome, described in step 4.
Where the line waited, the command has already printed the outcome. read_status still returns the whole record, such as the health check's evidence and the console lines the command never prints.
To wait without sleeping, add wait_seconds, a whole number from 1 to 45. The call then waits to respond until no deploy, promote, or redeploy of the application is in flight. It checks every two seconds and waits at most 45 seconds from when the call arrived. The response adds settled, true where nothing was in flight when the call last checked, and waited_ms, the time it waited. settled: false means a deploy, promote, or redeploy is still in flight, not that it failed, so call it again. Without wait_seconds, call it every ten seconds.
A deploy typically takes up to about two and a half minutes on a development pod and up to about three and a half on its own container app. A promote over a serving version takes at least 33 seconds and typically up to about 45. So a promote usually needs one wait and sometimes two, a deploy to a pod two to four, and one to its own container app up to five.
Production's container pulls the application's image under an identity the platform creates for the application. The application's first deploy creates it, so a first promote does not wait for it. Where the identity does not exist yet when you promote, the promote creates it and waits 30 seconds for it to become usable before creating production's container. That promote takes about a minute and needs two waits. A first deploy to development's own container app takes the same 30-second wait.
A second waiting call for the same application returns at once, a deploy's or promote's own wait included, as does a call during a platform restart. A value outside 1 to 45 is refused 400 invalid_request, its detail giving 45.
The response's first member, summary, says what the call found in one sentence per environment. Each gives the state and the serving version, and for a record in flight its version, step, and seconds so far. Where an environment's rotated_since_read or bound_not_applied lists a setting, the next sentence counts them, and settings_apply gives the remedy. Where pending_upload contains an upload, a sentence says one is pending and names it. The summary then says which environment the top-level members describe.
Its top-level members describe production and the application: the state, the recorded version, the plan, the minimum replica count, or warm floor (warm_floor), the hostname, the egress mode, and the hosting cell (cell). Where the call names an environment, they describe that one. Two more are connection_limit, the plan's database connection limit, and database, that environment's database row without its secret. The limit is present whatever the manifest declares, and it governs the application's client pool once the manifest declares the database kind; read_plan_quotas returns every plan's. The read_status reference describes every member.
The top-level environment member gives the environment the top-level state, compute_state, version, and hostname describe: the one the call named, or production. The hostname is that environment's own, also before its first deploy. For development, warm_floor is that environment's floor: 1 on pro, 0 on free and standard. During a deploy to production or a promote, production's state is deploying. After a failed first one, it is failed.
The environments object contains one object per environment the application has, production alone or development and production:
| Member | What it contains |
|---|---|
state |
The same word list_applications returns for the environment: deleting, halted, deploying, deployed, failed, or never_deployed. The application says when each applies. |
compute_state |
The hosting service's own word for the environment's compute, listed below. Null until a deploy or promote has placed compute. |
version, hostname, cell |
The serving version, the environment's hostname, and its hosting cell. |
grain |
container or pod. |
grain_reason |
unplaced while no compute exists, so grain shows a placeholder. chosen once a deploy or promote has placed the compute in that grain. |
grain_note |
One sentence saying what the grain means for this environment. |
halted |
Null while the environment runs. Otherwise the halt's time at and its author by, developer or platform. |
deleting_at |
The time a deletion of the environment or the application began, while the deletion runs. |
deploy |
The environment's in-flight or most recent history record, described below. |
rotated_since_read |
Each setting the running copy applied whose secret was stored or rotated after that copy started, with the secret it reads. restart_application applies the stored value. A rotation during a build is listed once, and the restart clears it. |
bound_not_applied |
Each setting the manifest binds that the running copy lacks, with the secret it reads. The environment's next deploy or promote applies it, and a restart does not. |
pending_upload |
Null on an environment a deploy does not go to, and where no upload waits. Otherwise the upload whose zip landed within a day and that no deploy has read. It contains its id, its state, and retry, the deploy call that starts it without a new upload, with the commit a refused start named, null where the zip itself was refused. The state is not_started, or refused with refusal containing the start's error and detail. |
database |
The environment's database row without its secret: the database name, the role name, the server host, the provisioning time, and the environment. Null where the environment has no database. |
The development database row appears once a submission provisions it, before any promote. On one environment it is the top-level local_run_database, the database local runs use. With tables: true in the request, tables lists that database's table names. Where the read failed, tables is null and tables_error says why.
Development runs as a pod, one of many on the cell's shared cluster, which keeps an idle development copy cheap. Production runs as its own container, and so does development where the cell accepts no pods. Before a first successful deploy, grain is container and grain_reason is unplaced, also after a failed first deploy. Every deploy chooses the grain again, and grain_reason then is chosen.
Either grain runs the same image with the platform's settings, and a pod also sets HOME to /tmp and runs your process as uid 1000. Where the cell's shared development group is full, a deploy is refused group_full. Each environment's grain_note says in one sentence what its own grain means, or that no compute exists yet, so a grain that changes between deploys explains itself.
The compute_state depends on the grain:
- For a
container, it is the state the hosting service reports. That state isDeletingwhile the platform removes the container, andunavailablewhere the platform could not read it. - For a
pod, it isRunning,Idle,NotFound, orUnknown.Idlemeans no copy runs, after the idle interval or during a halt. The next request starts the pod unless the environment is halted.NotFoundmeans no compute exists, andUnknownmeans the cell's cluster could not be read.
The deploy record states id, version, and these members:
| Member | What it contains |
|---|---|
kind |
deploy, promote, or restart, a restart of the running copy by restart_application, a rename, or the platform. |
state |
deploying, deployed, or failed. |
artifact_hash |
The artifact's SHA-256. Null where the version predates the version history. |
harness_hash |
The SHA-256 of the runtime harness in the version's image. Null where the platform did not record it. |
harness_current |
True where that harness is the one the platform puts in new images. |
started_at, ended_at |
When the action started and ended. |
declarations_read_at |
When the action read the manifest's declared rows. A restart keeps the time of the version it restarts, so this can be earlier than started_at, and a schedule declared since then first fires after the next deploy or promote. |
worker_heartbeat_at |
When the platform's deploy worker last reported the work alive. It tracks the worker, never your application's health. |
step, step_started_at |
Present while the state is deploying: the step the work is in, such as image_build, compute_apply, or health_gate, and when that step began. Null until the work records its first step. |
gate_progress |
Present while the state is deploying, and null outside the health check. During the check it contains polls, the probes made, timeout_ms, the check's bound, and last. That is the last probe's status, or its connection word as error. It never contains a body or an address. |
outcome |
Null while the row is deploying. On an ended row it starts with result: succeeded, failed, interrupted, or superseded. A failed row also includes error, detail, and step where the platform knows it. It adds gate for health_gate_failed and build for image_build_failed (step 4). list_versions returns the same outcome. |
timings |
The steps the record entered, in order, each with its started_at and ended_at. The last step ends when the record ends, and its end stays null while the record runs. It also stays null where a platform restart's sweep or a deletion ended the record. Null on a record written before the platform kept them. |
list_applications lists your applications, each with an environments array containing one object per environment. The application describes its members.
Check the release
A release ends with a check of production, after a deploy or a promote reaches it. Once production shows deployed, your tool requests each changed route that reads on the production hostname. A route that reads changes nothing. A GET with a side effect, such as sending mail or calling a paid upstream, counts as one that writes, and so does a route your tool cannot place. It requests a changed route that writes only where you say a request is safe for production's data.
It then calls read_logs with the container source for production's errors right after the requests, and once more about a minute later where its detail says lines may still be in the ingestion. A container's lines can take up to about a minute to reach the log store (Read logs and counters).
On a failure, it tells you whether the release applied a migration, since a rollback moves code and never a migration's data. It then rolls back with roll_back on your word. Roll back describes the action. As Add a database says, a migration stays applied after a rollback, so write migrations the previous version can run against.
Turn on a development environment
Development is a second hosted copy, at <label>-dev.ai.host, where each version runs before your users see it. create_environment, with the application identifier and environment: "development", turns it on before or after submit_manifest, with no browser approval.
Then a deploy goes to development and promote moves a version to production. Where the manifest declares the accounts kind, local runs sign testers in on a new development realm. Applications and environments describes it and delete_environment, which turns it off.
Promote to production
On one environment, promote is refused 409 environment_not_created, since the deploy already reaches production.
On two environments, promote requires the application identifier and takes two optional members:
version, the number of a deployed history row of the application. Where it is absent, the development environment's serving version is promoted. Where the development environment runs no version, the call is refused 409nothing_to_promote.rotate_database_credential, described below.
A named version is refused in two cases:
- It is not the number of a deployed row, or its row was retired when its environment was deleted: 404
no_such_version.list_versionsreturns the rows a promote can name. - The platform has deleted its image: 409
version_image_pruned.list_versionsshowspromotableandharness_currenton each row.
The platform keeps the images of each environment's twenty most recent deployed versions, its serving version among them, and deletes the rest once a day. To promote an older version, deploy its artifact again.
A promote is also refused while production is halted (409 target_environment_halted) and while a deploy, promote, or redeploy of production is in flight (409 deploy_in_flight). It is refused while a deletion of the application or of either environment runs (409 deletion_in_progress).
The promote takes the same wait_seconds as a deploy and responds the same way. Without it, the promote returns at once with status 202 and state: "deploying". With it, the response waits for the promote to end, within the wait. Its environment is always production. Where something other than the platform responds, call read_status first, and promote again only where it shows no promote started after your call.
The promote builds nothing: it reuses the image built at the deploy, so the code reaching production is the code the development environment ran. The image also contains the runtime harness from the deploy, so a promote keeps that harness, and only a new deploy takes the current one. Where the manifest declares the database kind and no production database exists yet, the first promote provisions it on a production server.
The promote then works in this order:
- It mints a new production platform credential and gives it to the new container. The current value keeps working until the switch.
- With
rotate_database_credential: true, it sets a new password on the production database role before the new container starts. - It starts the new version's production container beside the serving one, with production's settings and credentials. They include
TURNZERO_CLOUD_REALM_KEYSwhere production has a realm. - It polls the health path on the new container alone. On a 200 it switches the production hostname to the new container. A first promote records
deployedat once. Where production already serves a version, the switch waits until the promote is 30 seconds old, anddeployedcomes 3 seconds after it, once every router has the switch. - It removes the previous container about a minute after the switch: the router's resolve interval, 30 seconds by default, plus a 30-second grace.
The switch to the new platform credential happens only when the health check passes, so a failed promote leaves the current credential in place. The previously serving container keeps its platform credential until the platform removes that container. So the platform's services do not refuse a request that still reaches it.
A database rotation is the exception. From the rotation until the switch, a new database connection from the previous container fails, while its open connections continue. So the rotation runs on request, not at every promote.
From the switch, production's schedules restart from the promote. A scheduled run still in flight on the previous container can fail, and its next due time runs again. With wait_seconds, the promote's response includes the outcome where the promote ends within the wait. Otherwise make the next call it gives, read_status with wait_seconds, or check the row in list_versions.
A failed promote ends failed with the outcome named. Its new container is removed, and the serving one is untouched. A promote interrupted by a platform restart before its switch ends failed with the outcome interrupted within about fifteen minutes. One interrupted after its switch ends deployed, because the new version is already running.
A promote of the version production already runs is accepted. It mints a new production platform credential, and a new database password with rotate_database_credential: true, and applies the settings again.
Run locally
Your machine can run the application under its development records, so local test data never lands in the live database. Your tool calls submit_manifest with the application, its manifest, and local_run: true. It runs the response's provisioning.command, or provisioning.command_windows on Windows, once, as given, in the application's folder, before provisioning.expires_at, five minutes after the call.
The line reads echo <grant> | npx -y <address> provision --application <application id>. It re-mints the two development credentials, each once, and writes .env, or the file --env-file names. A second run needs a new line from a new submission naming local_run. Running on your machine describes it. Development's deployed copy keeps the revoked credentials until you deploy or restart it.
The application reads the file's seven settings at start:
APP_DATABASE_URL, the development database's connection string, built from the connection facts and the new password withsslmode=verify-full;APP_DATABASE_CONNECTION_LIMIT, the plan'sconnection_limit, the client pool's maximum, as a deploy sets it;TURNZERO_CLOUD_TOKEN, the development platform credential;APP_ENVIRONMENT=local, the log stream a local run writes;TURNZERO_CLOUD_API=https://turnzero.ai, the platform origin the application's sign-in and account reads use;TURNZERO_CLOUD_GATEWAY_URL=https://turnzero.ai, the origin the storage, logging, and external-API calls use, which the platform handles at the same address;TURNZERO_CLOUD_APPLICATION, the application's identifier, as a deploy sets it.
An application whose manifest declares no database kind has neither APP_DATABASE_URL nor APP_DATABASE_CONNECTION_LIMIT, and the line writes the other five. An application that reads none of these settings runs on your machine without the line. A hosted deploy never needs it.
Start the entry your start script runs with the environment file loaded, for example node --env-file=.env src/main.mjs for the project step 3 shows, and never commit the file. Two settings need care on your machine:
PORT: a deployed copy receives it from its image, and the line writes none, because the port is your application's own. Set it for the local run, for examplePORT=3000in the shell or a line of your own in.env, unless the server falls back to a port of its own. A server that requiresPORTdoes not start without it.- A variable already set in your shell wins:
node --env-filekeeps it and ignores the file's line. ATURNZERO_CLOUD_TOKENleft in the shell from another project would reach the platform in place of the file's credential. Before the run, unset in the shell each setting the file supplies, or start from a new shell.
Under the development platform credential, every call the local process makes is a development call:
- A file write is stored in the development partition of its area.
- A log line goes to the
localstream, whichread_logsreads withenvironment: "local". - A call to a declared upstream goes through the public gateway, which applies the development key value, so your machine has no provider key.
An end-user session is verified through the platform's verification route, POST /accounts/v0/verify, under the development platform credential, and not on your machine. The environment file contains no realm keys. Local verification would also need a header the serving router adds, which never reaches a process outside the platform, so keys in the file would change nothing.
On one environment, testers sign in on production's realm at <label>.ai.host/__account/ during a local run too, as end users of the live application. With development on, they sign in on the development realm.
The local process reaches the development database over the server's public endpoint under the platform's developer network rule. A local run loads no runtime harness. The Node.js Runtime package says which protections only a deployed copy receives. Applications and environments compares the local process, the deployed development copy, and the tests over the doubles.
What the application runs under
The deployed backend runs under the platform's hosting and network controls. An application can return APP_VERSION, its version number, on its health path to show which version responds. The copy receives its own identifier as TURNZERO_CLOUD_APPLICATION, so code that declares a storage area bound to the application reads it there, not from a copy in the code.
The running image is Docker Hub's node:24-bookworm-slim, pinned by digest: Node.js 24 and its npm, /bin/sh for npm start, and a minimal Debian 12 on Linux x64 with glibc. It contains no compiler, git, Python, or curl.
It runs JavaScript, code compiled to JavaScript or WebAssembly, and native modules. A native module is compiled on node:24-bookworm at the image build or prebuilt for Linux x64 with glibc, and can use only the slim image's shared libraries. The build loads each one and fails on one that does not load (the load check). A module opening a library only at first use fails at that use.
Egress firewall and request limits explains the network controls, the request deadline, the limits, and the diagnostic records.
Settings and environment variables a deployed copy receives
This section is the one list of the settings, the environment variables, the platform injects into a deployed copy. A deploy sets these settings on the copy of the environment it reaches, and a promote sets them on the production copy. A platform redeploy and a restart_application set them again on the copy they re-create. Your code may read every setting in the table.
A platform re-creation made while the plan's connection limit is not set includes no APP_DATABASE_CONNECTION_LIMIT, and your log gets a warn event saying so. A pool reading the setting as the Database package's composer does then uses one connection until the next deploy, promote, or restart_application.
| Setting | What it contains | When it is present | Code may read it |
|---|---|---|---|
TURNZERO_CLOUD_API |
The platform origin, for sign-in, management calls, and read_account. |
Always. | Yes. |
TURNZERO_CLOUD_APPLICATION |
The application's identifier, the value create_application returned. |
Always. | Yes. |
TURNZERO_CLOUD_GATEWAY_URL |
The origin for storage, egress, and logging calls. Without it, send those calls to TURNZERO_CLOUD_API. |
Where the platform gives a separate origin. | Yes. |
TURNZERO_CLOUD_REALM_KEYS |
The end-user realm's public key set, as JSON. | Where the environment has a realm. | Yes. |
TURNZERO_CLOUD_TOKEN |
The environment's platform credential, the bearer for storage, egress, and logging calls. It is kept secret. | Always. | Yes. |
APP_DATABASE_URL |
The database connection string, with its password. It is kept secret. | Where the environment has a database. | Yes. |
APP_DATABASE_CONNECTION_LIMIT |
The plan's connection_limit, the database connections each process may hold open at once: set the client pool's maximum to it. The role accepts twice that number. |
Where the environment has a database. | Yes. |
APP_ENVIRONMENT |
development or production. |
Always. | Yes. |
APP_VERSION |
The version applied, the integer read_status and list_versions show. |
Always. | Yes. |
APP_PUBLIC_HOST |
The environment's public hostname, never the request's Host. After a rename, the rename's restart writes the new hostname; where the restart is refused, it keeps the old one until the next deploy or promote. |
Always. | Yes. |
PORT |
8080, the port the server listens on. |
Always. | Yes. |
HTTPS_PROXY, HTTP_PROXY, NO_PROXY |
The proxy settings, in upper and lower case, for a client that needs an explicit proxy agent. The runtime harness sets them at start, so read them at run time. | Where outbound calls go through the platform's proxy. | Yes. |
A running copy keeps the values it started with. After set_plan moves the plan, or platform staff change the plan's value, APP_DATABASE_CONNECTION_LIMIT takes the new value at each environment's next deploy, promote, or restart_application. Add a database says what a move to a smaller plan does before then.
No setting contains an issue-tracking space or its token. A backend reaches its application's space through the egress gateway, which presents the space's token for it; the Issue Tracking package states how.
The platform sets other settings for its own use, and your code never reads them: HOSTING_WINDOW_REQUEST_SECONDS, HOSTING_WINDOW_SCHEDULE_SECONDS, HOSTING_WINDOW_GRACE_SECONDS, HOSTING_SCHEDULE_PATHS, EGRESS_PROXY, EGRESS_PLATFORM_ENDPOINTS, APP_ASSETS_BASE, NODE_OPTIONS, and npm_config_update_notifier.
The router mark's setting, ROUTER_MARK_SECRET, is the platform's too. From the platform release that includes hosting 0.9.0, the harness removes it from the environment before your request listener runs. Where the harness cannot write its file, it keeps the setting and writes one router_mark_setting_kept line under container.
A version built with an earlier harness keeps the setting until a new deploy. On two environments, production keeps it until that new version is promoted. read_status and list_versions return harness_current per version, which shows the versions behind.
A value you store with store_secret is not a setting unless the manifest binds it. The egress gateway applies an upstream's key at its edge on a call to the upstream declared with its name, and the key never enters the copy.
The manifest's settings member adds your own settings to the table's. Each gives a setting and the stored name whose value the setting receives, and your code may read it. The deploy and the promote read the value stored for their environment and inject it, kept secret like the platform credential.
A platform redeploy re-applies the settings the running copy was started with, and leaves out one whose stored name is gone, writing a warning to the application's log. Where the name still exists and the store does not respond, the redeploy ends failed bound_setting_unreadable, and the running copy keeps running. Store a secret states how to bind one.
A declared upstream's settings add one or two more, and your code may read them. The base-URL setting contains the gateway's address for that upstream. The key setting contains an egress key, kept secret: a credential the platform creates for that upstream and this copy alone. The deploy, the promote, and a platform redeploy each create fresh egress keys from the upstream declarations existing then. Call an external API with an API key shows how an unchanged SDK uses them.
Deploying a new version
A new deploy is the same call and line of step 3 with a new build, followed by the read_status wait as above. On two environments, a promote then moves the new version to production. A new deploy is also how a version gets the platform's current runtime harness, because its image is rebuilt. A version-history row of kind restart is not a new deploy: it restarts the running copy from the same build.
A development deploy gives the new copy the same development platform credential and mints no new one. Only the egress keys of upstreams that name a key setting are created afresh. apply_migration answers 501 not_yet_provisioned, served by no build; Backend versions and snapshots explains.
Bringing an existing application
The move-existing-app skill brings an application built elsewhere onto Turn Zero Cloud. When you ask for that, your tool:
- Asks you which plan the application starts on,
free,standard, orpro, and callscreate_applicationwith the name and the plan, as step 1 describes. - Writes the manifest and calls
submit_manifestwith the whole document, as step 2 describes. - Stores each key the application uses through the line
store_secretreturns, never as a tool argument. Store a secret describes the line. - Calls
declare_upstreamfor each API the code calls on a key. An SDK reading its base URL and key from settings runs unchanged on the settings the declaration lists. Call an external API with an API key shows the calls. - Builds the application and calls
deploy. It then runs the line the call returns, which uploads the zip, starts its deploy, and waits for it, as step 3 describes. Where the command returned before the outcome, it makes thenextcall,read_statuswithwait_seconds, until the deploy ends. - Where you turned development on, it moves the version to production through
promotewithwait_seconds, as Promote to production describes.
Expected result
After step 3, the command's start has returned status 202 with state: "deploying", the version number, and the production hostname <label>.ai.host. read_status with wait_seconds then shows production's deploy.state as deployed and its version as that number. The command itself printed each step, then settled: deployed version with that number, and ended with status 0. The production hostname returns status 200 on the health path.
list_versions shows one deploy row for production, with serving: true, promotable: true, and harness_current: true. list_applications shows one environment, production, deployed.
With development turned on, the deploy reaches <label>-dev.ai.host, and the promote adds a production promote row.
Your tool reports the hostname the version runs at and the version number. The project contains manifest.json. Where you also ran the application locally, it also contains .env, which is not committed.
Refusals
A refusal changes nothing on the platform. The status is the one the HTTP route returns, and the MCP tool returns the same name. Eight entries in the table, image_build_failed, build_wait_exceeded, health_gate_failed, slot_busy, interrupted, bound_setting_unreadable, platform_credential_unreadable, and database_credential_unreadable, are outcomes a history record states after the deploy starts, not refusals of the call. Manage versions and environments lists the refusals of rolling back, halting, renaming, and deleting. The generated reference lists each action's refusals in full, deploy among them.
| Refusal | Status | Cause | Remedy |
|---|---|---|---|
name_taken |
409 | create_application: the account already has a live application with that name. |
Choose another name; list_applications shows the application that has it. |
free_application_limit |
409 | create_application named free, and the account already has its one live Free application. Nothing is created. |
Choose standard or pro, or first move the Free one to another plan with set_plan. |
beta_plan_limit |
409 | During the beta, create_application named standard or pro, and the account already has its one live application on that plan. Nothing is created. |
Choose another plan, or first move that application to another plan with set_plan. |
plan_quantity_unset |
409 | A quantity of the named plan is not set, named in plan and measure, such as the database-connection-limit a deploy or promote of an application with a database reads. Nothing is added. |
Choose another plan, or wait until platform staff set the quantity. |
cell_unavailable |
503 | create_application: no hosting cell has room for the application. |
Retry later. |
cell_not_configured |
503 | No hosting cell accepts the action. create_application, submit_manifest, deploy, and declare_storage_area can return it. |
Retry later. |
manifest_invalid |
400 | submit_manifest: the manifest does not validate, and violations lists the failing JSON paths. deploy and promote also refuse a recorded manifest that binds a setting name the platform reserves. |
Correct each path against The manifest and submit the whole document again. |
environment_unsupported |
400 | submit_manifest: the manifest has an environments member. create_environment and delete_environment set the environments, never the manifest. |
Remove the member and submit again. |
database_provisioning_failed |
502 | submit_manifest: creating the development database role or password failed. |
Submit the manifest again. |
session_credential_required |
403 | mint_token was called under a minted token. It accepts the session credential alone. |
Mint the token for an unattended provision from your signed-in session, bounded to the application. For other calls, the token you already have may work. An application-scoped token works on the storage, egress, logging, and action routes within its scope. An account-scoped token works on the storage routes, keyed upstreams, and the action routes. The logging routes and the platform's ai-allowance upstream require an application-scoped token. |
area_scope_refused |
403 | mint_upload_grant: the area belongs to another application, or it is an application's deploy area, which no credential of yours reaches. |
Name the application the area belongs to, or an area bound to the application you name. For a deploy, call deploy with no artifact and run the line it returns. |
deploy_area_unavailable |
409 | deploy with no artifact: an area you declared already exists under the application's deploy area name, deploy-<application id>. It is left as it is. |
Deploy with artifact from an area of your own, uploaded under a grant from mint_upload_grant. The existing area and its files are yours and stay untouched. |
app_credential_not_admitted |
403 | mint_upload_grant was called under the application's platform credential. |
Call it from your connected tool, or under a minted token. |
deploy_code_refused |
403 | The line's code is unknown, expired, or already used: an earlier run of this line used it, or another party did. | Call deploy again; where previous_code names a started upload whose id no prepared: line of yours printed, roll back, rotate the application's secrets and its database credential, and report it. |
transfer_grant_spent |
403 | The upload grant's one write already landed, so these bytes were not written, or a later line's upload replaced its upload, or, for the start, the grant already made its one start. The same zip sent again after its upload landed returns 200 instead. For 30 seconds after a replacement, a gateway that checked the grant answers the same zip as a repeated write, writing nothing. | Where a later line replaced the upload, follow that line's run. Otherwise call deploy with no artifact and run its line, or mint_upload_grant for a file of your own area. Where read_status lists the upload as pending_upload with a retry, make that call. |
transfer_grant_not_admitted |
403 | A call under the upload's grant named another application, environment, or upload, named no upload or an artifact, or called another action. Nothing was spent. | Run the line as the deploy call returned it. For another action, use your connected tool. |
progress_window_ended |
403 | The command's progress read came after its window: five minutes after the deploy ended, or fifteen after the start. A start that was refused, or an upload a later line replaced, has none. | Read the deploy with read_status and wait_seconds. |
transfer_grant_expired |
403 | The upload grant expired before its upload or its start, five minutes after minting by default. | Call deploy with no artifact and run its new line at once, or mint_upload_grant. Where the refusal's detail gives a start-only retry, make that call instead. |
environment_required |
400 | An artifact upload under a minted token named no environment in the x-turnzero-cloud-environment header. |
Add the header giving the environment of the deploy that follows, and upload again. |
identity_required |
400 | An artifact upload under a minted token named no acting identity in the X-Acting-Identity header. |
Add the header with an application-chosen string distinct from the credential, and upload again. |
unknown_environment |
400 | deploy on the HTTP API: the environment is neither development nor production. Over MCP, the argument's own validation refuses such a value before the call. |
Name production, or development once it is turned on. |
environment_not_created |
409 | The application has one environment, and the call named development or was a promote. |
Deploy to production, or call create_environment first. |
environment_unavailable |
409 | deploy named production on an application with development turned on, where production receives a version through promote alone. |
Deploy to development, then promote. |
manifest_missing |
409 | deploy: the application has no manifest on file. |
Call submit_manifest first, as step 2 describes. |
deletion_in_progress |
409 | The application or one of its environments is being deleted. | Wait for the deletion to finish; read_pending_action shows whether it completed. If it failed, request the same deletion again to complete it. |
artifact_area_mismatch |
403 | deploy: the named area belongs to another application, to no application, or is not declared, or it is the application's deploy area, which artifact never names. |
Upload the artifact into an area declared with the application you deploy, and deploy again naming that area. For a zip in the deploy area, name its upload instead. |
upload_not_found |
404 | deploy with upload: no file the upload's own grant wrote exists. It was not uploaded yet, its grant expired unused, a later line's upload replaced it, or a deploy that has ended already read it. |
Call deploy with no artifact, run the line it returns, then make its next call. Where the detail says a deploy already read the upload, check that deploy with list_versions instead. |
local_route_required |
409 | deploy through the MCP tool named zip_sha256 or withdraw, which the turnzero-cloud command alone names, on the HTTP route. Nothing was prepared. |
Call deploy again naming the application and none of zip_sha256, artifact, and upload, and run the line it returns. |
upload_hash_mismatch |
409 | An upload sent a zip whose SHA-256 is not the one its preparing call named. Nothing was written, and the grant is unspent. A start reading such a file is refused the same way. | Call deploy again with no artifact and run the line it returns. |
artifact_not_found |
404 | deploy: no file with the named area and name in the account's partition. |
Upload the artifact to the named area's partition of the environment deployed, and name the area and the file exactly. |
artifact_hash_mismatch |
400 | deploy: the stored file's SHA-256 differs from the one the request names. |
Recompute the uploaded file's hash, or upload the build again, and deploy with the matching hash. |
artifact_unreadable |
400 | deploy: the artifact's bytes are not a zip, or an entry the deploy reads inflates past the size it declares. No version is added and no deploy counted. |
Zip the project folder's contents, upload the zip, and deploy again. |
artifact_layout_invalid |
400 | deploy: the zip breaks a layout rule step 3 states. Examples are no root package.json, no start script or root server.js, a refused entry name, a size past a limit, or a native module without its .node file. The detail names the cause and the entry, or, where exactly one top-level folder of the zip contains a package.json, that folder. No version is added and no deploy counted. |
Zip the folder containing package.json, from inside it, with a start script naming the server, and deploy again. On Windows, build the zip with the tar.exe command step 3 gives. Ship the root's or a workspace member's native module prebuilt, with its .node file where step 3 says. |
health_path_unserved |
400 | deploy to an environment that serves no version: no source file names the manifest's health path. Nothing is built and no deploy counted. |
Name the path in a source file, a comment included, and deploy the new zip. Or set the manifest's health to a served path, call submit_manifest, and deploy again. |
deploy_in_flight |
409 | deploy or promote: the environment already has a deploy, promote, or platform redeploy in progress. |
Wait with read_status and wait_seconds until deploy.state leaves deploying, then call again. |
setting_secret_missing |
409 | deploy or promote: a setting the manifest binds names a secret not stored at the environment's scope. The detail names the setting and the secret. A deploy or promote whose bound name is removed after the response ends failed under the same name. |
Store the value with store_secret, naming the application and the environment, then deploy or promote again. |
platform_minted_name |
409 | deploy or promote: a setting the manifest binds names one of the platform's own names, such as a platform credential. |
Store your own value under a name of your own, bind that name, and submit the manifest again. |
setting_is_upstream_key |
409 | deploy or promote: a setting the manifest binds names the key an upstream of the application uses. |
Remove the binding and call the upstream through the gateway, or bind another name, then submit the manifest again. |
name_bound_to_realm |
409 | deploy or promote: a setting the manifest binds names a sign-in method's credential of the application. |
Bind another name and submit the manifest again. |
name_bound_to_push |
409 | deploy or promote: a setting the manifest binds names a push provider's credential of the application. |
Bind another name and submit the manifest again. |
bound_setting_unreadable |
502 | A deploy, promote, or platform redeploy: a bound name exists at the environment's scope, and the store did not return its value. The record ends failed before anything is applied, and a running version keeps serving. |
Try again. If it repeats, report it with the detail. |
deploys_per_day_exceeded |
429 | deploy: the application's deploys in the last 24 hours reached the plan's deploys-per-day quantity. |
Wait until the oldest is more than 24 hours old, or move to a plan with a larger quantity. |
group_full |
409 | deploy: the hosting cell's development group is full. The detail names the group and its capacity. |
Retry later. |
group_kind_mismatch |
409 | deploy: the platform's placement record for the development environment is wrong, so nothing ran. |
Report it with the detail; the deploy succeeds once platform staff correct the record. |
deploy_seam_absent |
503 | deploy: the platform cannot start compute in the hosting cell at the moment. |
Retry later; nothing in your project causes it. |
platform_credential_unreadable |
502 | deploy or a platform redeploy: the platform could not read the environment's platform credential. |
Submit the manifest again naming local_run and run the line the response returns, which mints a new development platform credential. Then deploy development again. |
database_credential_unreadable |
502 | deploy, promote, or a platform redeploy: the platform could not read the application's database credential. |
Retry. If it repeats for development, submit the manifest again naming local_run and run the line the response returns, which mints a new development database credential. Then deploy development again. |
image_build_failed |
422 | The image build of the artifact failed, or ran past fifteen minutes, so nothing was applied. The record ends failed, and a running version keeps serving. |
Read outcome.build.output_tail through read_status or list_versions, fix the artifact, such as its package.json or lockfile, and deploy again. |
build_wait_exceeded |
503 | The image build waited fifteen minutes for its turn behind other builds and never started, so nothing was built or applied. The record ends failed, and a running version keeps serving. |
Deploy again, which takes a new turn. Where it repeats, the platform is busy with other builds, so wait a few minutes first. |
health_gate_failed |
502 | The new copy's health path did not return status 200 in time. The record ends failed, and a running version keeps serving. |
Read the record's outcome.gate through read_status or list_versions, fix the health path or the startup failure, and deploy again. |
slot_busy |
409 | The previous container was not removed in time, so nothing was applied. The record ends failed. |
Deploy or promote again. If it repeats, check read_status and the platform events through read_logs. |
interrupted |
none | A platform restart interrupted a deploy, promote, or platform redeploy before its switch. The record ends failed within about fifteen minutes. |
Deploy again with the same artifact. With the upload form, upload the zip again through a new deploy call and its line. |
nothing_to_promote |
409 | promote named no version, and the development environment runs none. |
Deploy to development first, or name a deployed version. |
no_such_version |
404 | promote: the version is not a deployed row of the application, or its environment was deleted. |
Name a row list_versions returns with state: "deployed". |
version_image_pruned |
409 | promote: the platform has deleted the named version's image. |
Deploy that version's artifact again as a new version, then promote it. |
target_environment_halted |
409 | deploy or promote while production is halted, or run_schedule on a halted environment. |
Call resume_environment on that environment, then try again. |
invalid_request |
400 | Missing or malformed input, such as a deploy without application, one naming both artifact and upload, or a wait_seconds outside 1 to 45. So is a deploy naming local_path beside either, or a local_path containing a character a shell would change. So is rotate_database_credential on a development deploy, or a preparing call or a start naming a value the line's call did not. The detail names the member, and for wait_seconds the bound 45. |
Correct the named member. |
no_such_application |
404 | The account has no application with that identifier. | Use an identifier list_applications returns. |
schedule_not_deployed |
409 | run_schedule: the environment has no deploy or promote made since the schedule was declared. |
Deploy to that environment, or on two environments promote to production, then run the schedule again. |
Related
- Manage versions and environments explains how to roll back, read the version history, halt and resume, rename, and delete.
- Applications and environments explains the hostnames, the environment states, the serving version, the halted state, and the two deletions.
- The manifest explains each manifest member, and Author the manifest shows how to write one.
- Store files describes the storage interface, the upload grant, the environment header, and an artifacts area of your own.
- Mint a token describes the token a script of your own runs under.
- Add a database explains the production database, the rotation request member, and what the migration walk does at start.
- Store a secret covers the keys an application brought from elsewhere needs.
- Read logs and counters explains
read_logs, which returns the console of the running copy and, for a while after a deploy, of the copy it replaced. - Egress firewall and request limits explains the controls the deployed backend runs under, and the Node.js Runtime package and the running image state the integration requirements.
- Plan and usage shows each plan's quantities, the daily deploy quota among them.
- Management actions is the generated reference for every action this page mentions.