Deploy

Sending a large project

Inline files are simplest, but a real project on disk should not be base64'd through your context. begin_upload streams a tarball straight to agenthost instead.

Inline files are the simplest thing that works, and the right choice for source you just wrote. They are a poor fit for a project that already exists on disk: every byte has to be base64-encoded into the request, which means reading the whole thing into your context first.

This path needs a shell

begin_upload hands you a URL to stream bytes at, which means running tar and curl (or an equivalent raw HTTP PUT) on the machine holding the files. If your client cannot run shell commands, stay on inline files.

For that, call begin_upload. It returns a single-use url and an upload_id. Stream a gzipped tarball of your project root at the URL — the bytes go straight over HTTPS, so they never pass through the conversation:

tar czf - --exclude=.git --exclude=node_modules --exclude=.env -C ./my-app . \
  | curl -sSf --upload-file - "$UPLOAD_URL"

Then pass upload_id wherever you would have passed files — to deploy_app, or to create_app, which infers the runtime from the uploaded tree just as it would from inline files. The two are mutually exclusive: send one or the other.

Archive rules

  • Paths are relative to the archive root. Create it from inside your project (tar czf - .) or with -C, so the tarball holds package.json, not my-app/package.json.
  • Regular files only. Symlinks are refused rather than followed, as are hardlinks and device entries. Use tar --dereference if your tree relies on symlinks.
  • .git/ is dropped. It is never part of a built app.
  • Plain tar works too — gzip is optional, just larger on the wire.

What a ticket is bound to

A ticket is not a general-purpose upload slot. It carries three bindings, all enforced when the deploy claims it:

  • One person. It deploys only for the user who called begin_upload — not for anyone else in the same organization.
  • One app. Pass service_id and the ticket deploys to that app and no other. Omit it and the ticket can only be used by create_app to make a new app; it can never overwrite an existing one's source.
  • One deploy. Claiming it consumes it. Deploying again means a fresh ticket and a fresh upload.

Send immediately — the url is a credential

The upload URL expires two minutes after minting, because it is handed to an agent and therefore lands in a model's context and whatever transcript that is written to. Run the tar | curl right away. Once your bytes land, the clock changes: you then have 15 minutes to deploy, because what identifies the upload from that point is a non-secret id with the bindings above.

Exclude what you would not commit

Dependencies and build output are installed and built on our side. Shipping node_modules, .venv, dist or .next will blow the file-count limit and slow the build down for no benefit — exclude them the way you would in a .gitignore.

Watch for secrets

tar czf - . sweeps up dotfiles, including a local .env. Everything in the archive becomes part of the app's source. Exclude it — --exclude=.env — and set those values at /secrets-manager instead, where they are stored apart from your source and injected at run time.

Size limits

PathLimit
Inline files5 MB of files
Upload ticket25 MB of files, 500 files

The inline limit is the smaller of the two on purpose, and going past it returns an error that points here rather than a transport failure. If you are near it, use an upload — it is not a workaround, it is the intended path for anything project-sized.