Apps Endpoints¶
Manage registered applications. Apps are uniquely identified by (app_name, environment, branch) — but the environment is not a request parameter: the API process is bound to a single environment and resolves it server-side.
Group scoping
Every route here is group-scoped. Callers see only their own group's apps;
a platform operator can target a specific tenant with ?group=<slug>.
A cross-tenant lookup returns 404, never 403 — the API never confirms
that another tenant's app exists.
Endpoints¶
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/api/apps |
read | List apps in the caller's group scope |
GET |
/api/apps/{app_name}?branch= |
read | Get app details |
GET |
/api/apps/{app_name}/branches |
read | List the branches an app is registered on |
GET |
/api/apps/{app_name}/branch-candidates?provider=&repo= |
read | The repo's branches, each stamped with what registration would do |
GET |
/api/apps/{app_name}/config?branch= |
read | Config in layers (base / overlay / effective / YAML seed) |
PUT |
/api/apps/{app_name}/config?branch= |
deploy | Save a config — as an object or as raw YAML |
POST |
/api/apps |
deploy | Register a new app → 201 |
PATCH |
/api/apps/{app_name}?branch= |
deploy | Update app configuration |
PATCH |
/api/apps/{app_name}/primary-services?branch= |
deploy | Set the per-service front-door override |
PATCH |
/api/apps/{app_name}/services/{service_name}/replicas?branch= |
deploy | Set a service's declared replica count and redeploy → 200 |
DELETE |
/api/apps/{app_name}?branch= |
admin | Delete an app registration → 204 |
POST |
/api/apps/{app_name}/redeploy?branch= |
deploy | Rebuild + deploy (or deploy the latest image) → 202 |
POST |
/api/apps/{app_name}/retry |
deploy | Retry a failed deploy's failing phase → 202 |
POST |
/api/apps/{app_name}/reconcile?branch= |
deploy | Re-assert the live route against visibility |
POST |
/api/apps/{app_name}/set-production?branch= |
deploy | Promote a branch to the project's production version |
POST |
/api/apps/{app_name}/preflight/recheck?branch= |
deploy | Re-run the git-repo preflight → 202 |
List Apps¶
Takes no required parameters. Platform operators may add ?group=<slug> to
target a specific tenant; an operator without one sees every app in the
environment.
Response: 200 OK
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"app_name": "my-web-app",
"display_name": "My Web App",
"slug": "my-web-app",
"group_slug": "acme",
"branch": "main",
"repository": "git@github.com:org/my-web-app.git",
"config_path": "appmecca.yaml",
"deploy_status": "running",
"visibility": "public",
"is_production": true,
"latest_build": "main-abc12345",
"build_count": 12,
"running_version": "abc12345",
"running_build_id": "0d809902-dd9d-4fdf-aa58-48be3b8a4e6a",
"running_since": "2026-03-15T09:12:00Z",
"urls": ["https://my-web-app-web-acme.dev.appmecca.net"],
"database_enabled": false,
"cache_enabled": false,
"email_enabled": false,
"preflight_status": "ready",
"created_at": "2026-03-10T14:30:00Z",
"updated_at": "2026-03-15T09:12:00Z"
}
],
"total": 1,
"domain": "dev.appmecca.net"
}
domain is the environment's base domain, returned once so clients don't have
to derive it. Each item is enriched server-side with urls (the app's live
front doors), running_version / running_since (from the last successful
container deploy — see the note on Get App for why the list and the
single-app route answer different questions, and for why both go null once
the app is undeployed), and deploy_warning when a newer deploy failed while
an older version keeps serving. ⚠ deploy_warning is suppressed for an
undeployed app for the same reason: it is only meaningful while something is
still serving.
Get App¶
Path parameters:
| Parameter | Type | Description |
|---|---|---|
app_name |
string | The app name or slug (sanitized, lowercase) |
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
branch |
string | the production branch | Branch name. Omit to resolve the project's production branch. |
Response: 200 OK — single AppResponse object (same shape as list items).
The single-app response additionally surfaces failure details so clients never have to join against builds, deployments, or logs to explain a failure:
| Field | Type | When populated |
|---|---|---|
last_deploy_status |
string \| null |
Status of the most recent deploy action on this app+branch (running, failed, deploying, etc.). |
last_deploy_error |
string \| null |
The error recorded for that action. Present on every non-successful deploy. |
last_crash_output |
object \| null |
{service_name: [line, ...]} with up to 30 lines per service. Only present when the latest deploy is failed AND the agent's health-check phase captured container crash output. Infrastructure-level failures (routing, image pull) leave this null. |
last_build_status / last_build_error |
string \| null |
Equivalent surfacing for the most recent build. |
running_version / running_since |
string \| null |
The image tag currently serving, and when it went live. Resolved per branch. ⚠ This route answers "what release does the front door serve?" and so covers both deploy shapes — a type: static app reports its static_deploy_completed tag here. GET /api/apps (the list) answers the narrower "what container is running?" and leaves these null for a static app; the two are deliberately different questions (BUGS #1008). Both include RETRY, so a successful appmecca app retry reports the retried tag, not the one before the failure. ⚠ Both are null once deploy_status is undeployed — an UNDEPLOY does not retract the completed DEPLOY row (it writes its own and flips the app's status), so without this the history row would report a tag for up to the 30-day prune window on an app where nothing is serving (BUGS #1187). The suppression is an exclusion of that one status, never an allow-list: failed and undeploy_failed still report a tag, because both leave containers serving and are exactly when you need to know which. null means not reported — never a guess. |
running_build_id |
string \| null |
builds.id of the build that produced the running release — the only field that identifies WHICH build is deployed. The image tag is the commit sha and is not build-unique: a rebuild of one commit produces a second build with the same tag, and under a mutable ECR repo the later push overwrites the earlier image, so the tag does not even identify the bytes (BUGS #1096). null means unattributable — a --no-build deploy (no depends_on), a RETRY or drain child (they carry parent_action_id instead), or a release row pruned at 30 days. It is also null whenever running_version is — the three retract together, so a build cannot be badged currently deployed on an undeployed app. Render unknown for null; do not fall back to matching tags. |
deploy_warning |
string \| null |
Set when a newer deploy failed while an older version keeps serving. |
urls |
string[] |
The app's live front doors. |
services[].url |
string \| null |
Per-service address, stamped at read time (list + by-name responses; BUGS #1234) — built by the canonical hostname builder in the same loop as urls, honouring the app's routing shape (path-routed: the front door bare, a mounted service with its mount suffix). null for a service the platform does not route (no port and not static, or unmounted under path routing). Stored per-service url/urls values are never echoed — they are rebuilt on every read (the flat-urls rule of BUGS #6b, applied per service). Absent only on an env with no configured domain. |
visibility |
string |
public or private. |
is_production |
boolean |
Whether this branch is the project's production version. |
The response also carries the git-repo preflight state — a newly-registered app is quarantined until its repo is proven buildable:
| Field | Type | Notes |
|---|---|---|
preflight_status |
string |
One of pending, no_access, needs_config, config_invalid, ready, exempt. Only ready and exempt unlock build + deploy — every other value quarantines the app (build/deploy/retry 409). Defaults to the fail-safe pending. |
preflight_result |
object \| null |
The structured verdict the dashboard quarantine view and mecca app show render — {reason, detail, services, missing_paths, errors}. |
Errors:
404 Not Found— app does not exist for the given name/branch, or belongs to another group.
List Branches¶
Returns every branch the app is registered on. Backs the -b branch selector
in mecca var and friends: the CLI expands a glob or comma-list against this
list, then issues the per-branch calls.
Response: 200 OK
{
"app_name": "my-web-app",
"environment": "dev",
"branches": ["main", "dev", "feature/dark-theme"]
}
?next_branch=<name> (FEATURES #437) — a branch you are about to register.
When sent, the response also carries:
| Field | Type | Meaning |
|---|---|---|
next_branch_would_be_production |
boolean | null | Registering this branch would make it the app's production branch. null = could not be determined. |
production_branch |
string | null | The app's production branch today (is_production → main → newest). |
next_branch_copy_refusal |
string | null | The sentence to show when nothing may be copied into this branch; null when a copy is allowed. |
clone_components_available |
object | {db, drives, buckets} — which data halves the app declares, so a client offers only those. |
A registration that copies from another branch is refused when the new branch
is or would become production, so a client should ask this first and show
next_branch_copy_refusal rather than work it out itself.
⚠ The keys' presence is the feature flag. An API older than FEATURES #437
omits them and silently ignores copy_vars_from_branch on register, so a
client must not offer the copy unless these keys come back.
List Branch Candidates¶
Every branch on the repository (not just the registered ones), each stamped with what registration would do with it. Backs the dashboard's "Deploy a branch" picker, which marks a branch that cannot be registered before the click rather than after the POST.
Session-only — listing branches uses the caller's own linked provider token, so
an api-key caller gets 401. provider must be a linked Git provider and
repo the repository's full name (owner/name, or a nested GitLab path).
Response: 200 OK
{
"app_name": "my-web-app",
"environment": "dev",
"rows": [
{
"name": "main",
"identity": "main",
"default": true,
"protected": true,
"committed_date": "2026-09-16T09:12:04Z",
"verdict": {"kind": "deployed"}
},
{
"name": "release/1.0.0",
"identity": "release-1-0-0",
"default": false,
"protected": false,
"committed_date": null,
"verdict": {"kind": "collides", "with": "release/1-0-0"}
},
{
"name": "feature/dark-theme",
"identity": "feature-dark-theme",
"default": false,
"protected": false,
"committed_date": null,
"verdict": null
}
],
"total": 3,
"truncated": false
}
verdict is null when the branch can be registered, or one of:
kind |
Meaning | What registration would return |
|---|---|---|
deployed |
Already registered on this app — this is the redeploy path | — |
collides |
Its derived identity is already taken by the named sibling branch | 409 |
label_too_long |
The hostname it would need exceeds max characters |
422 |
Predict, never enforce
The verdict is a courtesy, not a gate. It is computed by the same predicates the registration guards refuse on, but it can be stale — a sibling branch registered in another tab between this call and the POST. Clients must keep handling the 409 and 422 exactly as before.
Pagination
Branches are fetched to a cap of 500. total is the provider's count where it
reports one, and truncated is true when the cap was reached — surface both
rather than presenting a partial list as complete.
committed_date is GitLab-only; GitHub does not return a commit date on a branch
row, so it is null there. Do not sort by it as a primary key.
Register App¶
Scope: deploy or higher.
Request body:
{
"app_name": "My Web App",
"repository": "git@github.com:org/my-web-app.git",
"branch": "main",
"description": "Frontend + backend web application",
"config_path": "appmecca.yaml",
"database_enabled": false,
"cache_enabled": false
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
app_name |
string | yes | — | Display name, 1–128 chars (sanitized to slug internally) |
repository |
string | yes | — | Git repository URL |
display_name |
string | no | — | Explicit display name |
branch |
string | no | main |
Branch to track |
description |
string | no | — | Human-readable description |
config_path |
string | no | appmecca.yaml |
Path to appmecca.yaml in repo |
group_slug |
string | no | caller's scope | Register under an explicit group (alias: group). Goes through the same membership check as ?group=. |
git_clone_provider |
string | no | — | Clone with the caller's linked provider token (private-repo picker) |
deploy_config |
object | no | — | Deployment configuration |
services |
array | no | — | Service definitions |
containers |
array | no | — | Container definitions |
database_enabled |
boolean | no | false |
Whether the app needs a database |
cache_enabled |
boolean | no | false |
Whether the app needs a cache |
email_enabled |
boolean | no | false |
Whether the app may send email through the relay |
copy_vars_from_branch |
string | no | — | Copy env vars from this already-registered branch of the same app inside the registration, before the first build. Existing keys are never overwritten. 409 when the new branch is or would become the production branch (see List Branches ?next_branch=); 404 when the source is not a branch of this app in this group; 422 when the app has no other branch. The response carries copied_var_keys (key names only). |
clone_data_from_branch |
string | no | — | Restore this branch's data from another registered branch on its first deploy (a fresh backup of the source is taken then). Same 404/409 refusals as copy_vars_from_branch; 422 when combined with seed_from_reference; 503 until every deployment agent can run the copy. The response carries clone_data_scheduled: {source_branch, components}. |
clone_components |
object | no | {db: true, drives: false, buckets: false} field defaults; db + drives when omitted entirely |
{db, drives, buckets} for clone_data_from_branch. |
seed_from_reference |
string | no | — | One-shot seed source <app>:<branch>:<selector> (max 512), consumed on the first deploy. The source must be this app or an app in the same group: otherwise 404 (one message for not-found, other-group and ambiguous). Unparseable → 422. |
There is no environment field — the API resolves its own environment.
Behavior:
- The
app_nameis sanitized to a URL-safe slug (lowercase, hyphens). - A unique
slugis generated from the display name — if a collision exists, a numeric suffix is appended (e.g.my-app-2). - The authenticated user's
usernameis recorded ascreated_by. - The enablement flags (
database_enabled,cache_enabled,email_enabled) are derived fromdeploy_configwhen the caller omits them, and are re-derived from the repo'sappmecca.yamlon every deploy — so they self-heal. - A new app is quarantined at
preflight_status: "pending"until the git-repo preflight proves the repo buildable.
Response: 201 Created — the newly created AppResponse.
Errors:
403 Forbidden— the caller is not authorised for the requestedgroup_slug.409 Conflict— an app with this name/branch already exists; the branch collides with an existing one after sanitisation; or this repository is already registered in this group under a different app name (BUGS #1237). The last case fires only when creating a net-new(group, app_name)whose(repository, config_path)already belongs to another app in the same group — adding a branch of an existing app, and registering a second app from the same repo with its ownconfig_path(a monorepo), are both unaffected. Thedetailnames the existing app and both remedies. Comparison is normalised (scp/https/ssh://forms, trailing/,.git;config_pathNULL ≡appmecca.yaml,./-prefix and trailing/folded, case-sensitive — git paths are), group-scoped (the same repo registered by two tenants is legitimate), and skipped entirely when the repository cannot be parsed.409 Conflict—group_renamed_during_registration: the group's slug changed while the app's name was being derived;detail.group_slugis the current slug — retry against it. Alsogroup_deleted_during_registration(retry), orgroup_busy(another change to the group holds its lock; retry in a few seconds).404 Not Found— the group was deleted while the request waited for its lock; nothing was written.422 Unprocessable Entity— the repository URL is not on a supported git host.
Update App¶
Scope: deploy or higher.
Request body: partial update — only include fields to change.
| Field | Type | Notes |
|---|---|---|
display_name |
string | |
description |
string | |
config_path |
string | |
deploy_config |
object | |
services / containers |
array | |
database_enabled / cache_enabled / email_enabled |
boolean | |
seed_from_reference |
string | null | Max 512. null clears it — the only field this route accepts an explicit null for (every other null is ignored). A non-null value is refused 409 when the app has already been seeded or is running, 404 when the source is not this app or an app in its own group, 422 when it does not parse. |
visibility |
string | public or private. Changing it queues a route reconcile — the ALB rule is rewritten within about 15 s, and the app is fully converged within about 90 s (each gateway replica caches the hostmap for 60 s and expires on its own clock). No redeploy needed. A type: static app going public is slower: a private static app deliberately has no CloudFront distribution, so publishing has to create one — minutes, and the app's URL 404s until it lands. |
force_remove_domains |
boolean | Required to flip an app private while it has custom domains still attached; starts their teardown first. Only honoured on the app's production branch — domains are project-level, so tearing them down from another branch is refused. Without it the flip is a 409. |
Response: 200 OK — the updated AppResponse.
AppResponse.warnings is a (usually empty) array of non-fatal strings. A
visibility change queues the RECONCILE_ROUTING action that converges the
data plane; that enqueue is best-effort, because the column is already
committed by the time it runs and failing the request would report a flip that
DID happen as failed. When it does fail, the response says so here rather than
only in the server log. The agent's reconcile_static_visibility beat task
re-drives it automatically.
Errors:
404 Not Found— app does not exist.409 Conflict— flipping toprivatewith custom domains still attached and noforce_remove_domains; orforce_remove_domainson a branch that does not own them.503 Service Unavailable— the custom-domain check or a teardown failed. The flip is refused rather than risking a private app with a live custom domain (BUGS #705 — this guard used to fail open).
Config Layers¶
Returns the app's configuration in layers so a client can show which fields the repo owns and which the dashboard has overridden.
| Field | Meaning |
|---|---|
base |
The repo-authored appmecca.yaml, refreshed by the agent on every successful build |
overlay |
The dashboard-owned sparse field map ({path: {value, base}}) |
effective |
The merge the agent actually deploys from |
source_map |
Each edited path marked "dashboard" so the UI can badge it |
effective_yaml |
The effective config rendered as appmecca.yaml text, by the platform's own serializer — the seed for a raw-YAML editor. null when the app has no config at all, or when the render failed. |
effective_yaml is rendered server-side on purpose
It is produced by the same serializer that generates a config file for the
wizard, so the text a client shows is the text the platform itself would
write. A client that stringifies effective in JavaScript instead would be
a second derivation of the config shape — and JS and Python disagree on
exactly the literals configs contain (no, on, 22:30, 1.10).
Save Config¶
Scope: deploy or higher.
Saves the app's configuration. The body carries exactly one of two fields — sending both is refused, and neither is resolved by a precedence rule:
| Field | Type | Meaning |
|---|---|---|
config |
object | The full effective config, as an object |
config_yaml |
string | The same thing as appmecca.yaml text, parsed server-side |
Whichever door is used, the request converges on one pipeline: bound → validate
the merged config → refuse omissions → diff against the repo base → store
only what differs → bump preflight_nonce → re-queue GIT_PREFLIGHT.
Response
| Field | Meaning |
|---|---|
overlay |
The sparse {path: {value, base}} map that was stored |
changed_paths |
Paths added, changed or removed by this save |
applies_live |
Changed paths that take effect immediately |
requires_redeploy |
Changed paths that need a deploy |
preflight_action_id |
The re-queued preflight's action id — null means the re-check could not be queued. The save still succeeded; run POST /api/apps/{app}/preflight/recheck to get a verdict. |
Refusals (all 422)
| Cause | Detail |
|---|---|
Both config and config_yaml sent |
Send either config or config_yaml, not both. |
config_yaml is not a string |
config_yaml must be a string. |
| The YAML does not parse | The parser's own message, including line and column |
| The YAML parses to something other than a mapping | Names what a config must be |
| The config is too large, too deep, or aliases into an oversized graph | Names the limit that was hit |
| The merged config fails validation | validate_appmecca_yaml's errors |
| A field the repository defines was omitted | Names the omitted fields |
Omitting a field does not unset it
The overlay is applied on top of the repository's config, so there is no
way to express "unset this". A submission that omits a field the repo
defines is refused rather than silently accepted — accepting would report a
save that changed nothing. Remove the field in appmecca.yaml instead.
The body is bounded before anything walks it
YAML resolves an alias by sharing the referenced node, so a tiny document can expand exponentially once anything walks it. Measured: a 431-byte document parses in 2 ms and expands to 4.8 billion leaves. The request is refused in ~1 ms by a node-count bound applied before validation.
Primary Services¶
Scope: deploy or higher.
Sets which services are the app's front doors — the ones whose URL and preview image are surfaced on the app card. Body:
The override wins over the primary: field in appmecca.yaml and over
auto-detection, per service: a name you don't mention keeps its existing
verdict. Effects are immediate — no redeploy required.
Scale a Service¶
Scope: deploy or higher.
Sets how many hosts a service runs on and queues the redeploy that applies it. The count is written into the app's config overlay — the same store Save Config writes — so it survives until the next change, and the redeploy is a no-build deploy of the image the service is currently running.
That last point is deliberate, and is what the image_tag in the response is
for. The newest image an app has built is not always the one it is serving
— a rollback (mecca app deploy --image-tag <tag> --no-build), a
--no-deploy build, or a build whose deploy failed all leave the two
different. Scaling redeploys what is running, so it can never quietly ship a
release you had rolled back from.
Omit branch to resolve the branch the app is running on (falling back to
its production branch). Note that GET /api/services/{app} resolves main
first, so a client that scales without branch and then reads the service list
without one can be looking at two different branches — pass the branch from
the scale response.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
replicas |
integer | yes | How many hosts to run the service on, 1-3 |
replicas is strictly an integer — true and "2" are refused rather
than coerced, because a coerced boolean silently means 1 and would scale a
service DOWN on a 200.
The 1-3 bound is not re-stated on this route. It is enforced in one
place — validation of the merged config — so the route cannot come to disagree
with the schema about what a legal count is.
Response: 200 OK
{
"app_name": "euan-blog",
"branch": "main",
"service": "web",
"previous": 1,
"requested": 3,
"action_id": "…",
"preflight_action_id": "…",
"overlay_paths": ["deploy.web.replicas"],
"image_tag": "a1b2c3d"
}
| Field | Description |
|---|---|
previous |
the count the service declared before this call |
requested |
the count now declared |
action_id |
the queued redeploy, or null for a no-op |
preflight_action_id |
the re-queued config check, or null |
overlay_paths |
every config path the saved overlay now carries |
image_tag |
the running image the redeploy will deploy |
deployed |
(no-op only) the count actually running, or null if unknown |
action_id is null when the service already declares the requested count.
That is a no-op, not a failure: nothing is written and no deploy is queued,
so setting a value back to what it already was costs nothing. When the declared
and running counts differ, the no-op's message says so and names
mecca app redeploy — that is how a scale whose save landed but whose deploy
did not is told apart from one that was never needed.
Refusal bodies¶
Every refusal this route raises for a condition a client might branch on sends
detail as an object:
code is a stable machine token; message is a complete sentence naming the
remedy and is safe to show a user verbatim. Some codes carry extra
machine-readable keys. Refusals a client cannot act on programmatically (a
plain 404, a validation 422) keep a string detail.
code |
Status | Extras | Meaning |
|---|---|---|---|
nothing_running |
409 | — | the app is undeployed, or no container image is running on this branch, so there is nothing to scale. Nothing was written. |
scale_in_flight |
409 | action_id, action_type, status |
a container-class action is already in flight for this app and branch. Attach to action_id rather than polling blind. |
overlay_would_drop_fields |
409 | removed_paths |
a saved configuration override replaces a whole section, so writing this change would drop repository fields underneath it. Nothing was written; save the full configuration once from the Config editor, then scale. |
scale_saved_not_deployed |
500 (or the underlying status) | saved, requested, previous, deployed, reason |
the declared count WAS saved and the redeploy could not be queued. Run mecca app redeploy <app>. |
Errors:
404 Not Found— unknown app, or a service the app does not declare. Both are 404 so a caller who cannot see the app learns nothing from the status code; the bodies differ — the service refusal lists the app's declared services, which is safe because it is reachable only once the group gate has passed.409 Conflict— one of the structured codes above; or the app's configuration changed between this request's read and its write (another save, a scale, or a drift resolution landed first). Nothing was written in any case.409 Conflict— the app is quarantined by its config check, or its group is over its storage grant. These are the deploy's own gates and they run before anything is saved.422 Unprocessable Entity— the count is outside1-3, or is not a whole number; or the service is declaredsingleton: trueand runs on exactly one host by design; or the service is atype: staticsite served from S3 + CloudFront and has no container to replicate; or the service's storedreplicasis not a whole number, in which case the message names the field to correct. Each refusal names the remedy.503 Service Unavailable— the in-flight check could not be answered. Nothing was changed — retry.
⚠ Every refusal above leaves the stored configuration untouched. The one
exception is scale_saved_not_deployed, which exists precisely to say so.
CLI: mecca app scale <app> <service> <n>.
Delete App¶
Scope: admin.
Omit branch to resolve the app's live branch. This is a full purge: the
database is backed up then dropped, every child record is removed, and the
app's platform resources are cleaned up. Backup bundles are retained.
Response: 204 No Content.
Errors:
404 Not Found— app does not exist.409 Conflict— the app is registered on more than one branch and nobranchwas given (pass one explicitly rather than let the API guess which to destroy).
Redeploy App¶
Scope: deploy or higher.
Queues a rebuild followed by a deploy, and returns immediately.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
branch |
string | resolved | Branch to redeploy. Omit to resolve automatically: the running branch if exactly one is running, otherwise the app's production branch. Redeploy is reversible (it rebuilds and replaces containers; it does not drop the database or purge AWS), so an omitted branch resolves rather than being refused. Pass explicitly to target a specific branch. |
no_build |
boolean | false |
Skip the build and deploy the app's latest successful image |
skip_preflight |
boolean | false |
Bypass the git-repo preflight gate (escape hatch for a false-negative check) |
Response: 202 Accepted
{
"action": "deploy",
"action_id": "550e8400-e29b-41d4-a716-446655440000",
"app_name": "my-web-app",
"image_tag": "abc12345",
"message": "Deploy queued (no build) — using image tag abc12345"
}
The full rebuild path returns the build service's own envelope (build tag, action id and status).
Errors:
All non-2xx responses carry a structured detail body so operators can diagnose without grepping logs.
400 Bad Request— caller-side validation error, e.g.--no-buildwith no successful build to reuse.404 Not Found— app does not exist (on the resolved branch).409 Conflict— the app is running on more than one branch and nobranchwas given (pass one explicitly rather than let the redeploy pick); or the app is quarantined by preflight, or its group is over its storage entitlement.422 Unprocessable Entity— a typed build-service failure (config_path_required:,git_auth_missing:, or any other subclass). Thedetailprefix names the failure class.502 Bad Gateway—ecr_setup_failed:(build-project / registry provisioning failure).500 Internal Server Error— unexpected exception while triggering the build. Thedetailisbuild_service_error: <ExceptionType>: <message>so the operator sees what raised, never an opaque body.
Retry a Failed Deploy¶
Scope: deploy or higher.
An in-place retry of the failing phase of the app's most recent failed deploy. The retry reuses the failed attempt's placement, image tag and blue/green slot — it lands on the same host, in the same slot, rather than re-planning from scratch.
Response: 202 Accepted with the new action id.
Optional inputs (FEATURES #418):
| Input | Where | Meaning |
|---|---|---|
branch |
query | Retry the latest failure on that branch. Absent = the app's latest failure on any branch (unchanged). |
expected_action_id |
JSON body {"expected_action_id": "<id>"} |
The failed action the caller is looking at. The full stored id (36–256 characters, compared case-insensitively) — a UUID prefix is a 422. |
Sending neither still resolves the same row it always did (the app's latest failure, on any branch); both CLIs send neither. ⚠ Since BUGS #1417 that is no longer the whole story — the two refusals below run on every caller, with or without a body. See the envelope note under them.
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"expected_action_id": "3f0c5d1e-8b1a-4c3e-9d7a-2f6e1b0c4a55"}' \
"https://api.qa.appmecca.net/api/apps/shop/retry?group=acme&branch=main"
⚠ expected_action_id is checked BEFORE the retriability classifier, the
retry cap and the preflight gate. If it is not the latest failure, nothing is
queued and the answer is a 409 about the row you asked about — never a
retriability verdict about a different row:
{
"detail": {
"code": "retry_target_moved",
"message": "Not retried: the failed deploy you asked about (3f0c5d1e) is not the latest failure for 'shop' on branch 'main'. …",
"expected_action_id": "3f0c5d1e-8b1a-4c3e-9d7a-2f6e1b0c4a55",
"latest_failed_action_id": "9a1b…",
"latest_failed_branch": "main"
}
}
Two further refusals run before anything is queued. ⚠ Both apply to EVERY
caller (BUGS #1417) — with a body or without, from either CLI, the tenant
dashboard or the admin console. The order is fixed:
retry_target_ambiguous → retry_target_moved (body only) → retry_superseded
→ preflight → retriability → cap, so the answer always describes the earliest
thing that is wrong.
detail.code |
When |
|---|---|
retry_target_ambiguous |
The app name is registered in more than one group (BUGS #692). Failed deploys cannot be attributed to one of them, so no row is named. |
retry_superseded |
A later DEPLOY, RETRY or STATIC_DEPLOY of the same app and branch exists that did not fail and was not cancelled — including the first retry of this same failure. The check is repeated under an advisory lock on the app and branch in the same transaction as the insert, so two concurrent retries of one failure queue exactly one. A retry clones the failed attempt's image, placement and blue/green slot, so retrying the older failure would run the old image over the newer attempt. |
A mixed-shape app's own other half is not a supersession: a DEPLOY and the
STATIC_DEPLOY queued alongside it share one deployment_id, and the static
half is inserted second. Only a later attempt belonging to a different deploy
counts.
The ids named are the caller's own app's rows — the route is group-gated, and a name held by two groups is refused before any row is named.
The detail envelope depends on expected_action_id¶
Both refusals carry one sentence, in two envelopes:
- With
expected_action_id(the admin console),detailis an object —{"code": …, "message": …}, pluslater_action_id,later_action_typeandlater_statusforretry_superseded. Unchanged, byte for byte. - Without it (both CLIs, the tenant dashboard — including a body of
{}or{"expected_action_id": null}),detailis that same sentence as a plain string, the shape this route's other 409s already use. A released CLI prints an objectdetailas a key dump or a Pythondictrepr, so the string is what makes the remedy readable without a client upgrade.
{
"detail": "Not retried: a later DEPLOY (9a1b2c3d, completed) of 'shop' on branch 'main' exists. Retrying the older failure would reuse its image and blue/green slot over it. Redeploy instead."
}
Errors:
404 Not Found— no failed deploy to retry (onbranch, when given).409 Conflict—retry_target_moved,retry_target_ambiguousorretry_superseded(above); or the failure is not retriable (thedetailnames the classified reason and what to do instead), the retry cap is exhausted, or the app is quarantined by preflight.422 Unprocessable Entity—expected_action_idis shorter than 36 or longer than 256 characters.
Reconcile Routes¶
Scope: deploy or higher.
Re-asserts the app's live route against its current visibility without
changing any app state. Useful when you suspect drift and don't want to wait
for the periodic drift check. Returns the new action id.
Set Production Branch¶
Scope: deploy or higher.
Promotes one branch to be the project's production version. A "project" is the set of app rows sharing a repository within a group; at most one branch per project may be production, and the clear-then-promote runs in a single transaction so the invariant never breaks.
{
"app_name": "my-web-app",
"branch": "main",
"is_production": true,
"storage_identity_note": "Promotion does not move storage: each branch keeps its own drives and distributions. …"
}
Promotion does not move data
Each branch keeps its own drives and its own edge-cache distribution. Promoting a branch changes which one the project card features — it does not migrate storage.
Recheck Preflight¶
Scope: deploy or higher.
Re-runs the git-repo preflight — a clone-and-validate check that confirms the
env can reach the repo + branch, that an appmecca.yaml exists, that the
effective config is valid, and that the referenced Dockerfiles exist. Bumps
preflight_nonce and queues a fresh GIT_PREFLIGHT RPC (executed by the host
agent — the API is jailed from the git deploy key). The current
preflight_status is left untouched while the check runs (a ready app stays
deployable) and the new verdict overwrites it when the agent lands.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
branch |
string | main |
Branch to re-check |
Response: 202 Accepted
{
"action": "preflight_recheck",
"action_id": "3fa12ab8...",
"app_name": "my-web-app",
"preflight_status": "no_access",
"message": "Re-check queued — the verdict updates when it completes."
}
An exempt app (platform apps are born exempt) has nothing to check — the
response still carries 202 but reports preflight_status: "exempt" with
no action_id, since nothing was queued.
Errors:
404 Not Found— app does not exist for the given name/branch (cross-tenant probes 404, never 403).
Data Model¶
Apps are stored in the apps PostgreSQL table. See Schema Reference for the full table definition.
Key fields:
| Field | Description |
|---|---|
app_name |
Canonical name — {group-slug}-{app-slug} for a tenant app, bare for a platform app |
slug |
Branch-invariant identifier used in URLs and resource names |
group_slug |
The owning group (surfaced on the list endpoint so two tenants' identically-named apps are distinguishable) |
deploy_status |
Current status: registered, building, deploying, running, failed |
latest_build |
Tag of the most recent build |
build_count |
Total number of builds triggered |
deploy_config |
JSONB — deployment configuration from appmecca.yaml |
deploy_config_overrides |
JSONB — the dashboard-owned override overlay (config field map + primary front-door map) |
services |
JSONB — service definitions |
containers |
JSONB — container definitions |
ecr_uri |
The app's private image-registry URI |
seed_from_reference / seeded_at |
One-shot seed-from-backup source, and when it ran |