Skip to main content

Deploy source

Ship an app's source to Easel from a sandbox, a stream, or files you hold, and what Easel drops from every source.

Every publish sends the app's source to Easel, which builds it into a new deployment. Pick the way that matches where the source is when your user selects Publish:

Where the source isUse
A sandbox that can reach the internetAn upload URL
A sandbox with no outbound networkA streamed archive
Your database or object storageA file list

All three create the same kind of deployment, so you can switch later without changing anything else.

What Easel drops from every source

Whatever you send, Easel removes these before it stores or builds anything:

  • .env and .env.* files at any depth, except .env.example
  • .git, .hg, and .svn directories
  • macOS metadata files: .DS_Store and ._* files, which macOS tar adds to archives unless COPYFILE_DISABLE=1 is set

A .env file that generated code wrote is never deployed, so it's never served. Everything else you send is built, including node_modules, so leave out what the build doesn't need. The SDK's sandbox command leaves out node_modules. deployDirectory() and easel deploy also skip .vercel, .easel, and the patterns in the project's .easelignore.

From a sandbox with an upload URL

Your backend mints a single-use upload URL, and the sandbox uploads a gzip tarball of its working directory to it. No Easel token enters the sandbox: the URL is the only credential, it expires, and it works for one deployment.

import { sandboxUploadScript } from "@easel-sh/sdk";

const upload = await easel.deployments.createUpload({
  project: project.id,
  target: "production",
  meta: { appId },
  expiresIn: 300,
});

await sandbox.run(sandboxUploadScript(upload.uploadUrl));

const { deploymentId } = await easel.deployments.getUpload(upload.uploadId);

sandboxUploadScript() returns one shell command for a sandbox with tar, curl, and outbound HTTPS:

tar czf - --exclude='node_modules' --exclude='.git' --exclude='.env' --exclude='.env.*' . \
  | curl -sSf -X PUT -H 'Content-Type: application/gzip' -T - '<uploadUrl>'

The PUT has no Authorization header. Easel answers 202 with the new deploymentId, which the sandbox doesn't need to report back: ask getUpload() instead, so your backend doesn't trust output from the sandbox.

How an upload URL behaves:

  • Single use. Once a PUT creates a deployment, the URL is spent. A PUT whose body Easel rejects, such as one that's too large, isn't gzip, or was interrupted, doesn't use it up; fix the body and retry until the URL expires.
  • Tied to its token. The deployment is created as the token's user, with the target and meta you set when you minted the URL. If that token is revoked or expires, or its user leaves the workspace, the URL stops working.
  • Expires. Pick expiresIn to cover the time your sandbox needs to start uploading. For the allowed range, see Limits.

Don't log upload URLs; anyone who has one can deploy to the project until it's used or expires.

Stream an archive

When the sandbox can't reach the internet, run tar czf - . in it once and stream its stdout to Easel from your backend. Nothing is buffered on disk.

const tar = await sandbox.exec("tar czf - --exclude=node_modules .");

const deployment = await easel.deployments.create({
  project: project.id,
  target: "production",
  meta: { appId },
  archive: tar.stdout, // a ReadableStream<Uint8Array>
});

archive also takes bytes or a Blob. Without the SDK, POST the tarball as the request body with Content-Type: application/gzip; see Create a deployment.

A streamed body can only be sent once, so the SDK doesn't retry it after a network failure. Retry the whole publish with the same idempotencyKey to avoid a second deployment.

Send files you hold

When your platform stores each app's files, pass them as a list of paths and contents:

const deployment = await easel.deployments.create({
  project: project.id,
  target: "production",
  files: [
    { file: "index.html", data: html },
    { file: "api/entries.js", data: handler },
    { file: "logo.png", data: pngBytes },
  ],
});

The SDK sends small file sets inline in one request. For larger ones, it asks Easel which files it already has and uploads only the rest, so republishing an app with a few changed files uploads only those. See deployments.create for where the switch happens.

Choose the target and attach metadata

  • target: production or preview (the default). A Production deployment serves the project's production URL and domains once it's Ready. See Promote and roll back.
  • meta: string pairs of your own, such as your app's ID or version. They come back on the deployment and in every webhook about it, so you can match events to your records without a lookup. For size limits, see Limits.
  • prebuilt: send a .vercel/output directory you built yourself, and Easel skips the framework build.

Follow the build

deployments.create() returns as soon as the deployment is queued. Then either:

  • Receive deployment.succeeded or deployment.error with a webhook, or
  • Poll with deployments.wait(id, { onStatus }), which resolves when the deployment is READY, ERROR, CANCELLED, or another final status.

deployments.logs(id) returns the build log, and deployments.runtimeLogs(id) the requests the deployment served, with each function's first log line.

Limits

Sources share the build's upload limits, and upload URLs, inline requests, and meta have their own. See Limits and Platform API limits.

Next steps