Step 19: Redeploy

Phase: iterate

Context

You have updated app_files. Deploy the new version.

What to do

Redeploy via the same unified primitive as Step 15(await r.project(id)).apply(spec) (SDK 2.0+), run402 deploy apply (CLI), or the deploy MCP tool. Two redeploy modes:

Mode 1: full replace (most common)

Send the complete site again. Any files absent from site.replace are removed from the release. Subdomain reassign is automatic — keep the same subdomains.set name and the URL stays the same.

{
  "site":       { "replace": { /* ALL current files */ } },
  "subdomains": { "set": ["{subdomain}"] }
}

Mode 2: surgical patch (for small edits)

Patch only the files that changed. Set base: { release: "current" } to derive from the live release. Unchanged files carry forward; deleted files go in patch.delete.

{
  "base":  { "release": "current" },
  "site":  {
    "patch": {
      "put":    { "index.html": "<!doctype html>..." },
      "delete": ["old-page.html"]
    }
  }
}

Bytes already in CAS are not re-uploaded. Functions whose source / config didn't change are kept as-is — no Lambda churn.

SDK example (Node, @run402/sdk ^2.0.0)

const p = await r.project(env.PROJECT_ID);
const result = await p.apply({
  site: { replace: fileSet },
  ...(env.SUBDOMAIN ? { subdomains: { set: [env.SUBDOMAIN] } } : {}),
});
const url = env.SUBDOMAIN
  ? `https://${env.SUBDOMAIN}.run402.com`
  : result.urls.site;

The SDK auto-retries safe BASE_RELEASE_CONFLICT races when another deploy commits between your plan and commit. Don't hand-roll the retry loop.

Subdomain reassignment is automatic

Including subdomains.set: ["{subdomain}"] with the same name reassigns the subdomain to the new release atomically as part of activation. No separate API call. The subdomain_url stays the same — only the underlying release changes.

If you omit subdomains, the existing subdomain assignment carries forward unchanged.

If the deploy fails

The apply call throws Run402DeployError with a structured code and nextActions. Common cases:

  • INVALID_SPEC — typo in spec (e.g. site.replcae). Fix locally; nothing was uploaded.
  • MIGRATION_CHECKSUM_MISMATCH — you edited an already-applied migration's SQL. Use a fresh migration id.
  • MISSING_REQUIRED_SECRET warning — declared a secret in secrets.require[] that doesn't exist. Set it first via p.secrets.set(key, value) or POST /projects/v1/admin/{id}/secrets.
  • BASE_RELEASE_CONFLICT with safe_to_retry: true — the SDK already retried; if you see this, your deploy budget was exhausted. Reapply.

What to tell the user

If subdomain exists:

"Your app has been updated! Same link as before:

{subdomain_url}

Try the changes and let me know what you think!"

If no subdomain:

"Here's your updated app:

{deployment_url}

Try the changes and let me know what you think!"

Note: If you've been iterating many times, only share the latest URL.

Expected output

  • release_id — New release id from this deploy
  • deployment_url — Raw deployment URL (changes per release)
  • subdomain_url — Unchanged if subdomain was reassigned

Memory directive

Next step

Back to Step 17: Gather Feedback → (iterate loop)