Start with a product or guide

  • Start here

↑↓to moveto openEscto close
CloudServices

Services

A service runs continuously with your app. Use one for a poller, queue consumer or socket listener. Mezair starts it, restarts it according to your policy and stops it when the app stops. For a single background task, use a job; for a sequence of resumable steps, use a workflow.

Definition

TypeScript
import { app, service } from "@mezair-cloud/runtime";

export default app({
  capabilities: { services: true },
  services: {
    heartbeat: service({
      restart: "on-failure", // "always" | "on-failure" | "never" (default "on-failure")
      maxRestarts: 5, // default 5
      handler: async (ctx, controls) => {
        while (!controls.stopping()) {
          await ctx.log.info("tick");
          await sleep(1000, controls.signal);
        }
      },
    }),
  },
});

The handler receives the shared runtime ctx and a controls object:

  • controls.signal is an AbortSignal the platform aborts when the service must stop.
  • controls.stopping() returns true once a stop has been requested.

Handlers should observe the signal and return promptly. A handler that ignores it is given a bounded grace period (5 seconds by default), then recorded as stopped with a warning.

Services run in the background with no request auth context: ctx.auth.identity is always null inside service handlers.

Capability requirement

Declaring a service requires capabilities.services. Mezair Guard reports CAPABILITY_NOT_DECLARED during mezair cloud check and mezair cloud build if it is missing. The built Cell manifest lists each service as { name, restart, maxRestarts }.

Supervision semantics

The runtime supervisor records each service as running, stopped, failed, or restarting:

  • On handler failure, on-failure and always policies restart the handler with capped exponential backoff, up to maxRestarts. Exhausting the cap marks the service failed with the last error.
  • A clean return restarts only under always (the cap still applies); under other policies it marks the service stopped.
  • never marks the service failed on the first error without restarting.
  • Stopping aborts controls.signal, waits (bounded) for the handler to exit, and marks the service stopped.

Every transition is written through the host log adapter with kind: "service", so service lifecycle events appear alongside normal handler logs.

Local runtime

The local dev server starts every declared service on boot and stops them all when it shuts down. After each state transition it persists a status snapshot to .mezair/local/services.json. That file is informational; live state is in-process and served over HTTP:

Text
GET  /_mezair/services                ->  { "ok": true, "services": [...] }
POST /_mezair/services/<name>/stop    ->  { "ok": true, "service": {...} }
POST /_mezair/services/<name>/start   ->  { "ok": true, "service": {...} }

A service status record looks like:

JSON
{
  "name": "heartbeat",
  "state": "running",
  "restarts": 0,
  "startedAt": "2025-01-01T00:00:00.000Z"
}

Failed services additionally carry lastError: { code, message }.

CLI

Terminal
mezair cloud services list --json

This reads the .mezair/local/services.json snapshot, the last recorded states, not necessarily live. For live state, query GET /_mezair/services on a running dev server.

Current limits

Services execute fully on the local runtime. The AWS adapter can synthesize the intended ECS/Fargate resource shape, but preview deploy blocks service-bearing manifests with AWS_PREVIEW_UNSUPPORTED_FEATURE.

The current task scaffold does not run the exact Cell service handler. Blocking before provisioning avoids creating an idle, billable service while that runner is incomplete.

Single instance per service: no horizontal scaling, leader election, or distributed coordination in alpha.

Suggest an edit to this page on GitHub