{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://run402.com/schemas/release-spec.v1.json",
  "title": "Run402 ReleaseSpec v1",
  "description": "Authoring schema for Run402 deploy manifests. Use run402 up for complete application workflows. Project selection may be supplied by an explicit CLI target, app-local link or approved --name creation; the SDK requires a resolved project before gateway planning. SDK-native ReleaseSpec uses project; CLI/MCP manifests may use project_id.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Editor metadata only. Manifest adapters strip this before deploy planning."
    },
    "x-run402-omitted_features": {
      "type": "array",
      "description": "App-kit evidence metadata for humans/agents. Manifest adapters preserve it in the loaded manifest and strip it before deploy planning."
    },
    "project": {
      "type": "string",
      "description": "SDK-native project id."
    },
    "project_id": {
      "type": "string",
      "description": "CLI/MCP-friendly project id, normalized to ReleaseSpec.project."
    },
    "idempotency_key": {
      "type": "string"
    },
    "idempotencyKey": {
      "type": "string"
    },
    "base": {
      "$ref": "#/$defs/base"
    },
    "database": {
      "$ref": "#/$defs/database"
    },
    "secrets": {
      "$ref": "#/$defs/secrets"
    },
    "functions": {
      "$ref": "#/$defs/functions"
    },
    "site": {
      "$ref": "#/$defs/site"
    },
    "assets": {
      "$ref": "#/$defs/assets"
    },
    "subdomains": {
      "$ref": "#/$defs/subdomains"
    },
    "routes": {
      "oneOf": [
        {
          "type": "null"
        },
        {
          "$ref": "#/$defs/routes"
        }
      ]
    },
    "checks": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/smokeCheck"
      }
    },
    "i18n": {
      "oneOf": [
        {
          "type": "null"
        },
        {
          "$ref": "#/$defs/i18n"
        }
      ]
    },
    "verify": {
      "$ref": "#/$defs/verify",
      "description": "Authoring-only post-apply HTTP verification, run by `run402 up` after a successful apply and rerunnable with `run402 up verify`. Stripped before deploy planning (never part of the wire ReleaseSpec)."
    }
  },
  "$defs": {
    "base": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "release"
          ],
          "properties": {
            "release": {
              "enum": [
                "current",
                "empty"
              ]
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "release_id"
          ],
          "properties": {
            "release_id": {
              "type": "string"
            }
          }
        }
      ]
    },
    "database": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "migrations": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/migration"
          }
        },
        "expose": {
          "type": "object",
          "description": "Authorization/expose manifest. See https://run402.com/schemas/manifest.v1.json for its schema."
        },
        "zero_downtime": {
          "type": "boolean"
        }
      }
    },
    "migration": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "description": "Versioned immutable migration id, e.g. 001_init. Same id plus same checksum noops; same id plus different SQL is MIGRATION_CHECKSUM_MISMATCH. Use name instead for generated/idempotent SQL whose identity should track content changes."
        },
        "name": {
          "type": "string",
          "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$",
          "description": "Content-tracked migration name for generated/idempotent SQL. The SDK compiles this to <name>_<sha256(sql)[0:16]>; changed content applies once under a new id and identical re-deploys noop. SQL declared with name MUST be idempotent because it re-runs whenever content changes against a database where prior versions may already exist."
        },
        "checksum": {
          "type": "string"
        },
        "sql": {
          "type": "string"
        },
        "sql_ref": {
          "$ref": "#/$defs/contentRef"
        },
        "sql_path": {
          "type": "string"
        },
        "sql_file": {
          "type": "string"
        },
        "transaction": {
          "enum": [
            "required",
            "none"
          ]
        }
      },
      "oneOf": [
        {
          "required": [
            "id"
          ],
          "not": {
            "required": [
              "name"
            ]
          }
        },
        {
          "required": [
            "name"
          ],
          "not": {
            "required": [
              "id"
            ]
          }
        }
      ]
    },
    "secrets": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "require": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "delete": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "functions": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "replace": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/$defs/functionSpec"
          }
        },
        "patch": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "set": {
              "type": "object",
              "additionalProperties": {
                "$ref": "#/$defs/functionSpec"
              }
            },
            "delete": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "functionSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "FunctionSpec. schedule is a sibling of runtime/source/files/config, not config.schedule.",
      "properties": {
        "runtime": {
          "enum": [
            "node22"
          ]
        },
        "source": {
          "$ref": "#/$defs/fileEntry"
        },
        "files": {
          "$ref": "#/$defs/fileSet"
        },
        "entrypoint": {
          "type": "string"
        },
        "config": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "timeoutSeconds": {
              "type": "integer",
              "minimum": 1
            },
            "memoryMb": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "schedule": {
          "oneOf": [
            {
              "type": "string",
              "description": "5-field cron expression."
            },
            {
              "type": "null",
              "description": "Remove an existing schedule in patch mode."
            }
          ]
        },
        "deps": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        },
        "triggers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/functionTrigger"
          }
        },
        "requireAuth": {
          "type": "boolean"
        },
        "require_auth": {
          "type": "boolean"
        },
        "requireRole": {
          "oneOf": [
            {
              "type": "null"
            },
            {
              "$ref": "#/$defs/functionRequireRole"
            }
          ]
        },
        "require_role": {
          "oneOf": [
            {
              "type": "null"
            },
            {
              "$ref": "#/$defs/functionRequireRole"
            }
          ]
        },
        "class": { "enum": ["ssr", "standard"] },
        "capabilities": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          }
        }
      }
    },
    "functionTrigger": {
      "type": "object",
      "additionalProperties": true,
      "required": [
        "id",
        "type"
      ],
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1
        },
        "type": {
          "type": "string",
          "minLength": 1
        }
      }
    },
    "functionRequireRole": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "table",
        "id_column",
        "role_column",
        "allowed"
      ],
      "properties": {
        "table": {
          "type": "string",
          "minLength": 1
        },
        "id_column": {
          "type": "string",
          "minLength": 1
        },
        "role_column": {
          "type": "string",
          "minLength": 1
        },
        "allowed": {
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1
          },
          "minItems": 1
        },
        "cache_ttl": {
          "type": "integer",
          "minimum": 0
        },
        "on_deny": {
          "enum": [
            "envelope",
            "redirect"
          ]
        },
        "sign_in_path": {
          "type": "string",
          "minLength": 1
        }
      }
    },
    "site": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "replace"
          ],
          "properties": {
            "replace": {
              "oneOf": [
                {
                  "$ref": "#/$defs/fileSet"
                },
                {
                  "$ref": "#/$defs/localDirRef"
                }
              ]
            },
            "public_paths": {
              "$ref": "#/$defs/sitePublicPaths"
            },
            "embedding": {
              "$ref": "#/$defs/siteEmbedding"
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "patch"
          ],
          "properties": {
            "patch": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "put": {
                  "oneOf": [
                    {
                      "$ref": "#/$defs/fileSet"
                    },
                    {
                      "$ref": "#/$defs/localDirRef"
                    }
                  ]
                },
                "delete": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            },
            "public_paths": {
              "$ref": "#/$defs/sitePublicPaths"
            },
            "embedding": {
              "$ref": "#/$defs/siteEmbedding"
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "public_paths": {
              "$ref": "#/$defs/sitePublicPaths"
            },
            "embedding": {
              "$ref": "#/$defs/siteEmbedding"
            }
          },
          "anyOf": [
            {
              "required": [
                "public_paths"
              ]
            },
            {
              "required": [
                "embedding"
              ]
            }
          ]
        }
      ]
    },
    "sitePublicPaths": {
      "oneOf": [
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "mode"
          ],
          "properties": {
            "mode": {
              "const": "implicit"
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "mode",
            "replace"
          ],
          "properties": {
            "mode": {
              "const": "explicit"
            },
            "replace": {
              "type": "object",
              "additionalProperties": {
                "$ref": "#/$defs/publicStaticPath"
              }
            }
          }
        }
      ]
    },
    "publicStaticPath": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "asset"
      ],
      "properties": {
        "asset": {
          "type": "string",
          "minLength": 1,
          "description": "Release static asset path, not a public URL."
        },
        "cache_class": {
          "$ref": "#/$defs/staticCacheClass"
        }
      }
    },
    "staticCacheClass": {
      "type": "string",
      "description": "Known values: html, immutable_versioned, revalidating_asset. Unknown future strings may be returned by observability APIs and should be preserved."
    },
    "subdomains": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "set": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "add": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "remove": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "routes": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "replace"
      ],
      "properties": {
        "replace": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/route"
          }
        }
      }
    },
    "route": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "pattern",
        "target"
      ],
      "properties": {
        "pattern": {
          "type": "string"
        },
        "methods": {
          "type": "array",
          "minItems": 1,
          "items": {
            "enum": [
              "GET",
              "HEAD",
              "POST",
              "PUT",
              "PATCH",
              "DELETE",
              "OPTIONS"
            ]
          }
        },
        "target": {
          "oneOf": [
            {
              "$ref": "#/$defs/functionRouteTarget"
            },
            {
              "$ref": "#/$defs/staticRouteTarget"
            }
          ]
        },
        "pricing": {
          "$ref": "#/$defs/routePricing"
        },
        "acknowledge_readonly": {
          "const": true,
          "description": "Durable acknowledgement for intentional read-only final-wildcard function routes. Valid only when target.type is function, pattern ends in /*, and methods are limited to GET/HEAD."
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "target": {
                "type": "object",
                "properties": {
                  "type": {
                    "const": "static"
                  }
                },
                "required": [
                  "type"
                ]
              }
            },
            "required": [
              "target"
            ]
          },
          "then": {
            "not": {
              "required": [
                "pricing"
              ]
            }
          }
        }
      ]
    },
    "routePricing": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "mode",
        "amount_usd_micros",
        "pay_to"
      ],
      "properties": {
        "mode": {
          "const": "always"
        },
        "amount_usd_micros": {
          "type": "integer",
          "minimum": 1,
          "maximum": 9007199254740991
        },
        "pay_to": {
          "const": "org_default_payout"
        },
        "networks": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "default": [
            "mainnet"
          ],
          "items": {
            "enum": [
              "mainnet",
              "testnet"
            ]
          },
          "description": "Omit to accept production mainnet only. Include testnet explicitly for testnet payments."
        }
      }
    },
    "functionRouteTarget": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "name"
      ],
      "properties": {
        "type": {
          "const": "function"
        },
        "name": {
          "type": "string"
        }
      }
    },
    "staticRouteTarget": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "file"
      ],
      "properties": {
        "type": {
          "const": "static"
        },
        "file": {
          "type": "string",
          "description": "Relative materialized release static asset path, not a public path."
        }
      }
    },
    "smokeCheck": {
      "type": "object",
      "additionalProperties": true
    },
    "i18n": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "defaultLocale",
        "locales"
      ],
      "description": "Routed-locale-context release slice (v2.5+). Omit at the top level to carry forward from the base release; pass top-level i18n: null to clear; pass an object to replace. defaultLocale MUST be byte-identical to one entry in locales[].",
      "properties": {
        "defaultLocale": {
          "type": "string",
          "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$",
          "description": "Default locale tag. MUST be byte-identical to one entry in locales[]."
        },
        "locales": {
          "type": "array",
          "minItems": 1,
          "maxItems": 50,
          "items": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"
          },
          "description": "Supported locale tags. Non-empty, max 50 entries. Tags are opaque — only the safety regex is enforced (no BCP-47 semantic validation)."
        },
        "detect": {
          "type": "array",
          "maxItems": 10,
          "items": {
            "type": "string",
            "pattern": "^(accept-language|cookie:[!#$%&'*+\\-.^_`|~0-9A-Za-z]+)$"
          },
          "description": "Walked in order; first match wins. Defaults to ['accept-language'] when omitted, max 10 entries; [] is allowed and means 'always default'. Sources: 'accept-language' and 'cookie:<name>' (RFC 6265 cookie-name grammar)."
        },
        "unknownLocalePolicy": {
          "type": "string",
          "enum": [
            "reject",
            "pass-through"
          ],
          "description": "What to do when a detect-source signal does not match locales[]. 'reject' (default, backwards-compatible) falls through to the next detect source then to defaultLocale. 'pass-through' returns the lowercased, trimmed signal value verbatim — letting the consumer's app DB decide whether translations exist for the tag. Capability i18n-unknown-locale-policy (issue #413)."
        }
      }
    },
    "fileSet": {
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/fileEntry"
      }
    },
    "localDirRef": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "__source",
        "path"
      ],
      "properties": {
        "__source": {
          "const": "local-dir"
        },
        "path": {
          "type": "string",
          "description": "Local directory path resolved by the SDK/CLI from the manifest directory and stripped before the apply request."
        }
      },
      "description": "CLI/MCP/Node SDK authoring form for static site directories. This is never sent to the gateway wire API."
    },
    "fileEntry": {
      "oneOf": [
        {
          "type": "string"
        },
        {
          "$ref": "#/$defs/contentRef"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "path"
          ],
          "properties": {
            "path": {
              "type": "string"
            },
            "content_type": {
              "type": "string"
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "data"
          ],
          "properties": {
            "data": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "$ref": "#/$defs/contentRef"
                }
              ]
            },
            "encoding": {
              "enum": [
                "utf-8",
                "base64"
              ]
            },
            "content_type": {
              "type": "string"
            }
          }
        }
      ]
    },
    "contentRef": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "sha256",
        "size"
      ],
      "properties": {
        "sha256": {
          "type": "string"
        },
        "size": {
          "type": "integer",
          "minimum": 0
        },
        "content_type": {
          "type": "string"
        },
        "contentType": {
          "type": "string",
          "description": "Legacy SDK ContentRef spelling accepted by the normalizer; prefer content_type in manifests."
        },
        "integrity": {
          "type": "string"
        }
      }
    },
    "assets": {
      "type": "object",
      "description": "v1.48 unified-apply asset slice. Per-key assets are committed inside the same activation transaction as functions/site/secrets so a release flips atomically.",
      "additionalProperties": false,
      "properties": {
        "put": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/assetPutEntry"
          }
        },
        "delete": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Asset keys to remove at activation."
        },
        "sync": {
          "$ref": "#/$defs/assetSync"
        }
      }
    },
    "assetPutEntry": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "key"
      ],
      "properties": {
        "key": {
          "type": "string",
          "description": "Asset key under the project's asset namespace (no leading slash)."
        },
        "source": {
          "$ref": "#/$defs/fileEntry",
          "description": "SDK-input form. Mutually exclusive with sha256/size_bytes; the SDK normalizer hashes + uploads via /content/v1/plans."
        },
        "sha256": {
          "type": "string",
          "description": "Pre-uploaded CAS reference (wire form). Mutually exclusive with source."
        },
        "size_bytes": {
          "type": "integer",
          "minimum": 0,
          "description": "Required when using the wire form (sha256 set)."
        },
        "content_type": {
          "type": "string"
        },
        "visibility": {
          "enum": [
            "public",
            "private"
          ]
        },
        "immutable": {
          "type": "boolean"
        }
      }
    },
    "assetSync": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "prefix",
        "prune"
      ],
      "properties": {
        "prefix": {
          "type": "string",
          "description": "Prefix under which destructive sync operates."
        },
        "prune": {
          "const": true
        },
        "confirm": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "base_revision",
            "delete_set_digest",
            "expected_delete_count"
          ],
          "properties": {
            "base_revision": {
              "type": "string"
            },
            "delete_set_digest": {
              "type": "string"
            },
            "expected_delete_count": {
              "type": "integer",
              "minimum": 0
            }
          },
          "description": "Confirmation token echoed back from a prior plan; required to commit a destructive sync."
        }
      }
    },
    "verify": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "http": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/verifyHttpCheck"
          }
        }
      }
    },
    "verifyHttpCheck": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id"
      ],
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1
        },
        "path": {
          "type": "string",
          "minLength": 1,
          "description": "Request path resolved against the project public origin."
        },
        "url": {
          "type": "string",
          "minLength": 1
        },
        "expect": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "status"
          ],
          "properties": {
            "status": {
              "type": "integer",
              "minimum": 100,
              "maximum": 599
            }
          }
        },
        "expected_status": {
          "type": "integer",
          "minimum": 100,
          "maximum": 599,
          "description": "Snake-case alias for expect.status."
        },
        "retries": {
          "type": "integer",
          "minimum": 1
        }
      },
      "allOf": [
        {
          "anyOf": [
            {
              "required": [
                "path"
              ]
            },
            {
              "required": [
                "url"
              ]
            }
          ]
        },
        {
          "anyOf": [
            {
              "required": [
                "expect"
              ]
            },
            {
              "required": [
                "expected_status"
              ]
            }
          ]
        }
      ]
    },
    "siteEmbedding": {
      "description": "Framing opt-in using platform catalog keys, never origins. Remote validation checks the current catalog. Null resets to deny; omission carries prior state.",
      "oneOf": [
        {
          "type": "null"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "frame_ancestors"
          ],
          "properties": {
            "frame_ancestors": {
              "type": "array",
              "uniqueItems": true,
              "items": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9_-]*$"
              }
            }
          }
        }
      ]
    }
  }
}
