Skip to content

Atelier — Operate

Manage and monitor apps running on an Atelier platform via its REST API. This skill is for operating apps that already exist; it does not create or change application code (see the atelier-build skill for that).

Setup

  • ATELIER_API_URL — base URL of the instance. Choose the address by where the agent runs:
    • In-cluster (the agent runs as an Atelier app, e.g. Hermes): use the internal service address http://atelier-core.atelier.svc.cluster.local:8080. Cluster DNS resolves it directly — no hosts-file or ingress changes, and the portal hostname (atelier.home.arpa) does not resolve inside the cluster.
    • External (e.g. Claude Desktop on your machine): a hostname/IP that resolves on that machine — e.g. http://atelier.home.arpa (add it to that machine’s hosts file / DNS) or the node IP directly.
  • ATELIER_API_TOKEN — a persistent API token, sent as a bearer header. Mint one in the Atelier UI: Settings → System → API Tokens (this page is admin-only — you must be logged in as an Admin to create tokens). Pick the role the work needs: Viewer for read-only, Developer to also manage lifecycle/secrets. Tokens start with atl_.
Terminal window
curl -s "$ATELIER_API_URL/api/apps" -H "Authorization: Bearer $ATELIER_API_TOKEN"

Responses are JSON. 401 = token missing/revoked/expired. 403 = the token’s role is too low for that operation (see the Role column below).

Endpoint reference

Paths are relative to $ATELIER_API_URL. {name} is the app’s exact name — read it from GET /api/apps; it often carries a random suffix (e.g. notes-app-7b8f19). Any request with a JSON body must send Content-Type: application/json, or the server rejects it (this includes the DELETE secrets call, which takes a body).

Inspect (role: Viewer)

OperationMethod & path
List appsGET /api/apps
Get one app (status, URL, health)GET /api/apps/{name} — the response includes deployed_source_sha (the source commit the running build was built from — use this, not version, to tell what’s live), deployed_build_id, and ready (true once the rollout is healthy)
Build history / one buildGET /api/apps/{name}/builds (list, each with source_sha) and GET /api/apps/{name}/builds/{id} (one record)
Build logsGET /api/apps/{name}/build-logs
Runtime/pod logsGET /api/apps/{name}/logs — add ?service={service} to pick a service (e.g. backend/frontend/db); SSE, use curl -N (bound the stream with --max-time N if you only want a snapshot)
Resource metrics (CPU/mem)GET /api/apps/{name}/metrics
Active per-app alert (if any)GET /api/apps/{name}/alert — returns 404 when the app has no current alert

Multi-service apps

An app can run more than one container — commonly a web service plus a database (declared in the repo’s atelier-spec.yaml; see the atelier-build skill). When it does:

  • It has one Deployment and Service per service, named {app}-{service} — e.g. myapp-app and myapp-db. A single-service app just uses {app}.
  • Use ?service= on the logs endpoint to pick between them. When an app is unhealthy, the failing container is often not the one you assumed: an app that can’t reach its database looks like an app crash, so check the database’s logs too before concluding the app code is at fault.
  • Services reach each other in-cluster at {app}-{service}:{port}.
  • The app’s public URL routes to one service only (a frontend if present, else the first built one). A database is never publicly exposed.
  • A rebuild only rolls the services Atelier built. A stock image (a database) is left running across a git push, so don’t expect its pod age or restart count to change after a deploy — that’s correct, not a stale rollout.

Alerts (role: Developer)

The supervisor and reconcile passes raise alerts for things like pod crash-loops, unhealthy volumes, and registry capacity. Recent history is queryable; pass ?app=<name> to scope to one app — useful for answering “any alerts on app X lately?” without scanning the whole platform feed.

OperationMethod & path
Recent alert history (platform-wide, newest first)GET /api/alerts/history
Recent alert history for one appGET /api/alerts/history?app={name}
Unread alert countGET /api/alerts/unseen-count
Mark history “seen up to now”POST /api/alerts/seen {}

Response shape: {"alerts": [{ id, severity, title, body, dedup_key, source, raised_at, dispatched, suppressed, dispatch_error, app_name }, ...]}. The app_name field is the alert’s first-class app attribution — alerts about a specific app carry it; platform-wide alerts (registry capacity etc.) have app_name: null and are excluded by any ?app= filter.

Default limit is 50, capped at 200. Pre-existing alerts from before the app_name column was added will show app_name: null and won’t appear in per-app filters.

Lifecycle (role: Developer)

OperationMethod & pathBody
Pause (scale to zero)POST /api/apps/{name}/pause{}
ResumePOST /api/apps/{name}/resume{}
Redeploy (rebuild + roll, no code change)POST /api/apps/{name}/redeploy{}
Delete (archive)DELETE /api/apps/{name}

Secrets (role: Developer)

OperationMethod & pathBody
List secret keys (values never returned)GET /api/apps/{name}/secrets
Set / update secretsPUT /api/apps/{name}/secrets{"secrets": {"KEY": "value"}}
Delete secretsDELETE /api/apps/{name}/secrets{"keys": ["KEY1", "KEY2"]}

Setting or deleting secrets restarts the app’s pods. Both bodies require Content-Type: application/json.

Public access (role: Developer)

Toggles whether an app is reachable from the public internet via the platform’s Cloudflare Tunnel. When fully configured (platform tunnel credentials + a Cloudflare API token in Settings → System → Public Access), saving the toggle causes Atelier to create the Cloudflare DNS record and update the tunnel’s published-application list automatically — no manual dashboard work.

OperationMethod & pathBody
Read tunnel + per-app statusGET /api/apps/{name}— — the response includes public_hostname (string | null), public_exposed (bool), and public_url (https://<hostname> or null)
Platform tunnel statusGET /api/cloudflared (admin-only)— — returns {configured, status, api_token_configured, tunnel_id_hint}
Toggle per-app exposurePUT /api/apps/{name}/public-access{"enabled": bool, "hostname": "blog.example.com" | null}

Constraints:

  • hostname is required when enabled=true and must be a valid DNS-1123 hostname; rejected with 400 otherwise.
  • The hostname must be unique across apps on this platform; conflicts return 400 with a specific message.
  • CronJob apps cannot be exposed (no in-cluster Service) — 400.
  • Archived apps cannot be exposed — restore first.
  • Disabling preserves the hostname so the user can re-enable without retyping; pass {"enabled": false, "hostname": null}.

If the platform has no Cloudflare API token configured, the toggle still persists to the DB but routes do NOT propagate automatically. Use GET /api/cloudflared to check api_token_configured before promising the user “it’ll be live in 10 seconds.”

After enabling, verify it actually serves — DNS + the Cloudflare edge can be live within seconds while the tunnel route settles a beat behind: curl -sS -o /dev/null -w '%{http_code}\n' https://<hostname>/. A 200 means done. A 502 (especially after a ~30s hang) means the edge reached the tunnel but the origin didn’t answer — historically an origin service/port mismatch; if you see it persist, capture it for the platform team rather than assuming DNS. Note GET /api/cloudflared is admin-only, so a Developer-role token that can toggle exposure can’t read tunnel status — escalate if you need it.

Platform

OperationMethod & pathRoleBody
Get settingsGET /api/settingsViewer
Update settingsPUT /api/settingsAdminpartial settings object (Content-Type: application/json)
Ask Nova (platform assistant)POST /api/nova (SSE)Developer{"messages":[{"role":"user","content":"..."}]}

Nova returns pipe tables. Nova often answers with markdown tables. Anything downstream that doesn’t render tables (Telegram, plain-text channels, etc.) will mangle them — reformat as bullets / **bold:** value lines before forwarding.

Reaching deployed apps from inside the cluster

Apps deployed by Atelier — yours, Hermes’s, anything else running here — are reachable from any in-cluster pod via the apps namespace (atelier-apps), with the Service name and port depending on whether the app is single- or multi-image.

(For BYOC apps “single-image” maps to one Dockerfile at repo root; for v2-built apps it maps to a single-service AppSpec. The naming rules are the same.)

Single-image apps:

http://<app-name>.atelier-apps.svc.cluster.local ← port 80

The ingress-routed Service is just <app-name> and always exposes port 80, regardless of the container’s EXPOSE. So a single-container Vite/nginx app whose EXPOSE is 8080 or 3000 is still reached on port 80 in-cluster.

Multi-image apps (BYOC: frontend/Dockerfile + backend/Dockerfile; v2: two or more services in the AppSpec):

http://<app-name>-frontend.atelier-apps.svc.cluster.local ← port 80 (the ingress-routed Service)
http://<app-name>-backend.atelier-apps.svc.cluster.local ← port matches the backend's EXPOSE (typically 8000)

The Service Atelier wires the ingress to (typically frontend) gets port 80; other Services keep their container EXPOSE port. So a FastAPI/Express backend with EXPOSE 8000 is reached at :8000, not :80.

Two more things that surprise agents first time:

  • Namespace is atelier-apps, not atelier. The atelier namespace holds Atelier’s own core/API/UI; atelier-core.atelier.svc.cluster.local:8080 is the platform itself, not a user app.

  • Use the explicit URL fields on GET /api/apps/{name}. The response returns four of them — prefer these over the legacy url field (which is just the portal URL and is kept for back-compat):

    • lan_urlhttp://<name>.<portal_domain>. Serves the app at root via a per-app subdomain. Recommended for SSR / Next.js / Vite / Astro apps that emit root-absolute URLs (/_next/..., href="/about") — the portal proxy below strips its path prefix and breaks those. Requires the operator to have configured wildcard DNS for *.<portal_domain> (e.g. via dnsmasq, Tailscale Split DNS).
    • in_cluster_url — e.g. http://<name>.atelier-apps.svc.cluster.local:80, the address to call from inside the cluster.
    • public_urlhttps://<public_hostname> when the app is exposed via Cloudflare Tunnel, else null.
    • portal_urlhttp://<portal_domain>/apps/<name>; resolves only from machines with the hosts/DNS entry, and (being a path-stripping proxy) can break root-absolute SSR apps. Use lan_url for those instead.

    If you’re on an older instance that doesn’t yet return lan_url or in_cluster_url, reconstruct them from <name> (or <name>-<service>) + <portal_domain> / atelier-apps + the port rule above.

Common workflows

Diagnose an unhealthy app

Terminal window
curl -s "$ATELIER_API_URL/api/apps/$NAME" -H "Authorization: Bearer $ATELIER_API_TOKEN" # status/health
curl -sN "$ATELIER_API_URL/api/apps/$NAME/logs" -H "Authorization: Bearer $ATELIER_API_TOKEN" # recent pod logs
curl -s "$ATELIER_API_URL/api/apps/$NAME/metrics" -H "Authorization: Bearer $ATELIER_API_TOKEN" # CPU/mem

If a build failed (not a running pod), read GET /api/apps/{name}/build-logs.

Rotate a secret (the app restarts automatically)

Terminal window
curl -s -X PUT "$ATELIER_API_URL/api/apps/$NAME/secrets" \
-H "Authorization: Bearer $ATELIER_API_TOKEN" -H 'Content-Type: application/json' \
-d '{"secrets":{"API_KEY":"new-value"}}'

Updating your local skill copy

The Atelier platform republishes latest/skills.tar.gz whenever the canonical SKILL.md files in the repo change — typically with new gotchas, corrected examples, or guidance learned from real sessions. The version: field in each skill’s frontmatter bumps with every meaningful update.

Check for updates without downloading the bundle. A tiny manifest lives next to the bundle at https://tryatelier.blob.core.windows.net/tryatelier/latest/skills-version.json. Poll it (it’s a few hundred bytes) and compare against the version: in your local copies — only pull skills.tar.gz when one is ahead of yours:

{
"published": "2026-06-16T12:00:00Z",
"bundle": "https://tryatelier.blob.core.windows.net/tryatelier/latest/skills.tar.gz",
"skills": { "atelier-build": "1.1.0", "atelier-operate": "0.3.5" }
}

Cherry-pick rather than overwrite. If you have already evolved your local skill copy (e.g. you added a Python/FastAPI Dockerfile snippet for your stack, or annotated a section based on a build failure you debugged), a blanket re-pull of skills.tar.gz would overwrite those local refinements.

The safer pattern: when you see a new bundle version, ask the agent that uses the skill to compare the upstream against the local copy and propose specific additions to merge in. Example prompt:

“The platform’s atelier-build skill is now at v0.2.8 (you’re using v0.2.6). Look at the upstream at <bundle URL>, diff against my current local copy, and propose the specific additions that are worth merging. Skip changes that conflict with my local refinements; flag those so I can resolve them by hand.”

This keeps the curation human-driven for non-trivial conflicts while picking up genuinely useful upstream additions (corrections, new gotchas, new framework snippets) automatically.

Safety

  • These calls operate on a live platform. DELETE /api/apps/{name} archives a running app and PUT .../secrets overwrites credentials — confirm intent before destructive or credential-changing operations.
  • A token only grants its role’s access; a 403 means the task needs a higher-scoped token (mint a new one in the UI as an Admin).
  • Always read the app’s real name from GET /api/apps rather than guessing.