Xeonr Developer Docs

Apps

Replicated HTTP workloads with versions, hostname routing and scale-to-zero activation.

An app is the third workload primitive: a stateless HTTP service whose instances are interchangeable and individually disposable. An instance dying is capacity changing, not an error.

Apps have no writable state — no console daemon, no Exec, no read-write workspace. They may mount a workspace snapshot read-only.

App, version, instance

An app is long-lived and holds the operational envelope: scaling, resources, env, egress policy, access mode, hostname, key. A version is one immutable (environment, port, workspace snapshot) triple beneath it. Instances are pods.

Splitting them is what makes promotion a pointer flip and rollback free.

const app = await cp.apps.create({
  label: "api",
  scaling: {
    minInstances: 0,            // 0 = scale to zero
    maxInstances: 5,
    concurrency: 50,            // max in-flight requests per instance
    idleTimeout: "5m",
    activationTimeout: "30s",
  },
  resources: { cpu: "500m", memory: "512Mi" },
  access: "key",
  autoStart: true,
});

console.log(app.hostname, app.key);

CreateApp makes an app that serves nothing: a hostname, a key and an envelope. Give it something to run:

const version = await app.createVersion({
  environment,
  port: 8080,
  label: gitSha,
  promote: true,                // the ordinary deploy
  snapshot: snapshotId,         // optional, mounted read-only
  mountPath: "/data",
});

app.rollback() promotes the most recently superseded version — the whole of a rollback, since superseded versions are retained precisely so it costs a pointer write. app.update({ env }) sends only the fields you pass: a full message would re-apply access and autoStart on every deploy and silently re-widen an app that was deliberately locked down.

Version stateMeaning
READYServable; may be promoted
ACTIVECurrently taking traffic
SUPERSEDEDWas active; retained so rollback costs a pointer write

PromoteAppVersion moves the pointer atomically and returns immediately — it does not wait for the new version to be warm. Blocking would make a rollback slower than the outage it's fixing, and the activator starts an instance on the next request anyway.

DeleteAppVersion refuses the active version. Deleting a superseded one is how a rollback target is finally released.

Hostnames and access

Every app gets an assigned hostname, one label deep:

<app-id>-<namespace>.apps.containers.xnr.app

Assigned rather than chosen, for the same reason function names are: one shared registrable domain across tenants running untrusted code makes a chosen name a phishing primitive. Flat rather than nested so a single wildcard certificate covers every app in every namespace.

enum AppAccess {
  APP_ACCESS_UNSPECIFIED = 0,  // treated as KEY
  APP_ACCESS_KEY = 1,          // requires the app key
  APP_ACCESS_PUBLIC = 2,       // anyone may call the hostname
}

Unset behaves as KEY: a field nobody set must fail closed.

The key travels in X-Xeonr-App-Key, and the proxy strips it before forwarding. Deliberately not Authorization, for two reasons: an app's own users authenticate with Authorization, and the instance should never see the credential that fronts it.

curl -H "X-Xeonr-App-Key: $KEY" https://<app-id>-<namespace>.apps.containers.xnr.app/health

Unlike platform API keys, the app key is readableGetApp returns it, and the console shows it. Its scope is HTTP to one hostname, and being able to curl an app directly is what makes "is it the app, or the proxy in front of it?" answerable in one command. Reading it requires ROLE_OPERATOR; RotateAppKey issues a new one and invalidates the old.

If your product has its own edge with its own auth (as Xeonr Functions does), keep apps on KEY. A second public URL that bypasses your edge bypasses everything your edge does — including, with auto_start, the ability for a stranger to cold-start instances at will.

Activation and scale to zero

With min_instances: 0, an idle app has no pods. What happens on the next request depends on auto_start:

  • true — the proxy activates an instance and holds the request. The caller does nothing special. This is what you want: it removes the race where the caller acquires an instance and it gets reaped before the caller proxies to it.
  • false — a request to a cold app gets a legible 503, and the caller is expected to have started it.

AcquireInstance remains available for warmup — starting an instance at promote time, before any user request arrives:

const { address, coldStart, waitedMs } = await app.acquire({ wait: "10s" });

ReportLoad feeds idle-reap and predictive scale-up without an RPC per HTTP request, for edges that front an app themselves:

await app.reportLoad({ inFlight: 3, lastRequest: new Date(), rps: 12.5 });

app.tailLogs({ follow: true }) streams output merged across replicas, each chunk tagged with which one produced it — an app's logs cannot live only in its pods, because the instance that emitted the line you need is usually the one that died.

Observing

GetApp returns the app plus its live instances, derived from pods; WatchApp streams both.

Instance stateMeaning
PENDINGPod exists, not yet listening
READYAccepting requests
TERMINATINGBeing reaped

Readiness means "the server is listening", not "the pod started" — which makes "running but never ready" a visible state rather than a mystery.

Updating the envelope

UpdateApp changes scaling, resources, env, egress, label, access and auto_start in place, applying only the fields named in update_mask. Version identity — environment, port, workspace — is not here: promote a different version instead.

On this page