Cloud|

App status providers

Publish administrator-only traffic summaries, failed requests, and offline listing removal through a shared capability role.

4 min read Updated 2026-10-30 #capabilities#app-status#monitoring

Offer traffic and failed requests to Cloud's app administration by declaring appStatus in defineCapabilities(). Any application can provide this role. Core discovers providers through the capability catalog and shows each one's figures with its catalog name. Registry and Core data determine app identity, running state, permissions, and whether an app can be removed.

Declare the role

Import the six input and data schemas from @k2b/cloud/contracts. Declare two Queries and one Action, then point the role at their local IDs. This complete, stateless provider reports no measurements and forgets offline apps idempotently:

ts
import { defineCapabilities, listApps } from "@k2b/cloud";
import {
  AppStatusSummaryInputSchema, AppStatusSummaryDataSchema,
  AppStatusDetailInputSchema, AppStatusDetailDataSchema,
  AppStatusForgetInputSchema, AppStatusForgetDataSchema,
  type CapabilityExecutionContext,
} from "@k2b/cloud/contracts";
import { err, fail, ok } from "@k2b/cloud/server";

const adminError = (context: CapabilityExecutionContext) => {
  const authority = context.authority;
  const user = authority?.actor.kind === "user" ? authority.actor.user : authority?.actor.delegatedUser;
  return authority && user?.roles.includes("admin") && authority.accessSubject.type === "user" && authority.accessSubject.userId === user.id
    ? null : fail(err.forbidden("Administrator access required"));
};
export const monitoringCapabilities = defineCapabilities({
  protocolVersion: 2,
  appStatus: { summary: "status.summary", detail: "status.detail", forget: "status.forget" },
  queries: {
    "status.summary": {
      title: "Read app traffic", description: "Return traffic for selected apps.", openWorld: false,
      input: AppStatusSummaryInputSchema, data: AppStatusSummaryDataSchema,
      run: async (input, context) => adminError(context) ?? ok({ data: {
        apps: AppStatusSummaryInputSchema.parse(input).appIds.map(appId => ({ appId, traffic: null, links: [{ kind: "metrics", label: "Monitoring", href: "/admin/monitoring" }], lastSeenAt: null })), otherApps: [],
      } }),
    },
    "status.detail": {
      title: "Read app failures", description: "Return route totals and one page of failed requests.", openWorld: false,
      input: AppStatusDetailInputSchema, data: AppStatusDetailDataSchema,
      run: async (_input, context) => adminError(context) ?? ok({ data: { routes: [], failedRequests: [], nextCursor: null, totalFailedRequests: null } }),
    },
  },
  actions: {
    "status.forget": {
      title: "Forget app status", description: "Forget status records for an offline app.",
      input: AppStatusForgetInputSchema, data: AppStatusForgetDataSchema,
      destructive: false, openWorld: false, idempotency: "required",
      run: async ({ appId }, context) => {
        const denied = adminError(context); if (denied) return denied;
        const authority = context.authority;
        const kind = authority?.credentialKind === "invocation" ? authority.actor.delegation?.credentialKind : authority?.credentialKind;
        if (kind === "oauth" && !authority?.scopes.some(scope => scope === "write" || scope === "admin")) return fail(err.forbidden("Write scope required"));
        if ((await listApps()).some(app => app.id === appId)) return fail(err.badInput("Only offline apps can be forgotten"));
        return ok({ data: { forgotten: true } });
      },
    },
  },
});

Pass this declaration to app.start({ capabilities: monitoringCapabilities, fetch }). See App capabilities for the complete application startup pattern. Replace the empty results with your own measurements; keep domain reads and authorization inside your app. The example is checked in docs-site/examples/cloud-docs/platform-app-status.ts.

The role points to local IDs, not fixed operation names. Gateway's first provider uses app-status.summary, app-status.detail, and app-status.forget. Startup rejects incompatible declarations; registry readers omit incompatible roles, and consumers check compatibility before calling. Both Queries must be closed-world and person-readable. The forget Action requires idempotency.

Return summaries

AppStatusSummaryInputSchema accepts { appIds, window }. There must be 1–200 application IDs. window is 1h, 6h, 24h, 7d, or 30d. AppStatusSummaryDataSchema returns:

ts
{
  apps: [{
    appId: "inventory",
    traffic: { requests: 120, errors: 2, avgMs: 18.5 },
    links: [{ kind: "metrics", label: "Monitoring", href: "/admin/monitoring" }],
    lastSeenAt: "2026-10-30T12:00:00Z",
  }],
  otherApps: ["retired-inventory"],
}

traffic: null means no measurements, never zero requests. Counts are nonnegative integers; avgMs may be null. Providers choose their error classification and keep it consistent across summaries, routes, and details. Gateway counts status codes from 500 and requests with an error_kind.

Return at most four links per app, with root-relative /admin/ destinations. Set the optional kind to logs, metrics, routes, or capabilities so consumers can place links without inspecting provider paths or translated labels. A link without kind appears under More, using its label. Labels are final display strings in context.locale; Core forwards the request locale. Resolve labels through the provider's message catalog.

lastSeenAt is an optional ISO timestamp or null, describing when this provider last saw the app. It can differ from Core's registry time.

Gateway returns all four kinds: logs, telemetry filtered by app and observation window, gateway routes searched by app ID, and capability executions filtered by app ID.

otherApps contains up to 50 distinct IDs known to the provider that were not requested. Include their rows in apps to supply timestamps and figures; the array accepts up to 250 rows, covering 200 requested and 50 additional apps. Rows must name a requested app or one of otherApps, without duplicates. A provider may omit apps it does not know.

Return details

AppStatusDetailInputSchema accepts { appId, window, cursor? }. AppStatusDetailDataSchema returns:

  • routes: up to 200 { prefix, requests, errors } records for the app's registered routes;
  • failedRequests: up to 50 { at, method, path, status } records;
  • nextCursor: an opaque continuation string, or null when finished;
  • totalFailedRequests: an optional count or null when unavailable.

at is ISO time, status is an HTTP status, and path may be a route template. Cursors belong to one provider, app, and observation window. Gateway uses timestamp and ID ordering, preserving microseconds when requests share a timestamp. Raw request retention can be shorter than rollup retention, so a long-window summary can outlive its individual failed requests.

Preserve authority and the response budget

Every provider checks the incoming context.authority for administrator access for each Query and Action. Core passes the administrator's verified RequestAuthority through the existing capability invocation path, with no technical identity fallback. OAuth mutations require write or admin. A catalog entry is discovery, not permission.

Core gives one render a shared 1.5-second deadline, 512 KiB of catalog and response data, four concurrent calls, and eight providers. Discovery and all operations spend the same budget. Received invocation bytes count before JSON or result validation, including invalid replies. The shared server client supports request-local accounting through CapabilityCaller.consumeResponseBytes; the in-process catalog is charged by its serialized result size.

Core's reader retains usable results and returns failures with unavailable, denied, timeout, or invalid. Entries beyond 200 requested app IDs appear in truncated. Discovery failures use { appId: "core", name: "App status discovery" } as their source. Providers beyond eight remain visible as unavailable. Consumers show failures in place with a retry action.

Core maintainers create one createAppStatusReader() from packages/core/src/app-status.ts per SSR render, then use its readAppStatusSummaries(params) and readAppStatusDetail(params) methods. Use the same authority object and locale for both. Standalone functions create a fresh reader for one operation. For pagination with several providers, pass providerAppId along with that provider's cursor.

Forget an offline app

AppStatusForgetInputSchema accepts { appId }; AppStatusForgetDataSchema returns { forgotten: boolean }. The Action forgets the provider's retained app listing and stops any offline audit. It refuses a running app. An already forgotten app succeeds, so a failed multi-provider operation can be retried. forgotten: false is a failed removal from Core's perspective.

Core's administrator-only DELETE /api/admin/apps/:appId/listing keeps keys, tokens, grants, and subscriptions. It rejects a live app or a rejected registry entry at apps/<appId> with HTTP 409 and app_running. Rejected entries for other apps do not block removal. It hides the listing only after every reporting provider succeeds. An unavailable provider or failed forget returns HTTP 503 with forget_failed and keeps the app visible. Core retains reporting provenance so an offline provider cannot silently disappear from this step. A live registry lease makes a hidden app visible again.

Gateway keeps its existing DELETE /api/gateway/apps/:id endpoint for older clients. For rollout and the additive Core migration, see Build and deploy.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.