Add a service
Use this when a bag needs a process that keeps running: an HTTP API, a web
page, a listener for webhooks. A bag declares the service in bag.yaml, and
Barry’s service management starts it at login, assigns it a port, restarts it
when it exits, and checks its health. The example is counter, a bag with one
HTTP service behind the instance secret.
Before you begin
Section titled “Before you begin”- Create a bag. A service needs no
tools.ts, and a bag can have both. - To ship a native macOS or iOS app instead, see Build and register apps.
Declare the service
Section titled “Declare the service”name: counterdescription: Counts over HTTP, as an example of a bag that runs a service.
services: web: description: Counter API behind the instance secret entry: src/server.ts serves: http health: /health secrets: [BARRY_SECRET]The fields a service uses most:
| Field | What it does |
|---|---|
description |
Required. What the service does |
entry |
The TypeScript or JavaScript file to run, inside the bag. Barry runs a .ts file through tsx |
command, args |
A program to run instead of entry. Declare one launch form, not both |
serves |
http when the service listens. Barry assigns a loopback port and passes it as PORT |
health |
A path Barry requests to check the service. Needs serves: http or a socket |
secrets |
Secret names the service receives as environment variables |
env |
Non-secret KEY=VALUE literals |
autostart |
login, the default, or manual |
The manifest names the protocol, never the port. A service that exits has
failed, and Barry starts it again. A task that runs once belongs in a schedule
instead. The bag.yaml reference lists the
rest, including socket, user, and local-only.
Declare the dependencies
Section titled “Declare the dependencies”Barry runs a TypeScript entry with the tsx loader it finds in the bag’s own
node_modules, so a service bag depends on tsx as well as its server
framework:
{ "name": "counter-bag", "private": true, "type": "module", "dependencies": { "@barry-rocks/sdk": "link:../barry/sdk", "express": "4.22.1", "tsx": "4.23.12", "zod": "4.4.3" }, "devDependencies": { "@types/express": "4.17.21" }}Write the service
Section titled “Write the service”@barry-rocks/sdk/services/http holds the HTTP setup every Barry service
shares, and @barry-rocks/sdk/services holds listenOnAssignedPort:
import express from "express";import { z } from "zod";import { listenOnAssignedPort } from "@barry-rocks/sdk/services";import { applyAuth, applyBaseMiddleware, serve } from "@barry-rocks/sdk/services/http";
const Count = z.object({ count: z.number().int() });let count = 0;
const app = express();applyBaseMiddleware(app);
// Public routes go before applyAuth; everything after it needs the instance secret.app.get("/health", (_req, res) => { res.json({ ok: true });});applyAuth(app);
const router = express.Router();const routes = serve(router, "/api");routes.get({ method: "get", path: "/api/count", operationId: "getCount", responses: { 200: Count } }, (_req, res) => { res.json({ count });});routes.post({ method: "post", path: "/api/count", operationId: "increment", responses: { 200: Count } }, (_req, res) => { count += 1; res.json({ count });});app.use("/api", router);
const server = await listenOnAssignedPort(app);console.error(`counter listening on ${JSON.stringify(server.address())}`);The order of the calls is the security model:
applyBaseMiddlewaresets security headers, parses request bodies up to 1 MB, limits each IP address to 50 requests a second, and answers CORS requests from localhost and from the origins inBARRY_CORS_ORIGINS.- Routes registered next stay public, so the health check goes here.
applyAuthadmits only requests that carry the instance secret, as a bearer token or anx-barry-secretheader, and answers everything else with 403. It also answers 415 to a request that changes state and sends a body that isn’t JSON or multipart, which blocks form-based cross-site requests. The secret comes fromBARRY_SECRET, so the service declares it.serveregisters routes from declarations. Each declaration names its method, path, operation ID, and a zod schema for each response status. Under vitest, a response that doesn’t match its schema becomes a 500.listenOnAssignedPortlistens on127.0.0.1at the port inPORT, and refuses to start without one.
Write diagnostics to stderr, and read them with
barry service logs counter.web --stderr.
Reach Barry and other services
Section titled “Reach Barry and other services”A service process doesn’t host Barry, so before it calls the Barry object it
connects with Barry.connect(await localConnection()), importing
localConnection from @barry-rocks/sdk/services. localConnection reads the
instance’s service registry and its secret.
Call Barry from a bag shows
the calls.
To reach a core service directly, getServiceUrl("api") returns the API’s
origin. It reads the registry on every call, so the next request after
barry service port moves a service reaches the new address. It also takes
mcpBarry and slack.
Verify
Section titled “Verify”Install the bag, which starts its services, then check the service:
barry install ~/repos/counterbarry service status counterThe status shows counter.web running, with its port and a health of ok.
To get its address, run:
barry service url counter.webA request to /health at that address returns {"ok":true}. A request to
/api/count without the secret returns 403.
If it fails
Section titled “If it fails”- The status shows the service in backoff. The process keeps exiting.
Run
barry service run counter.webto start it in the foreground and read its error. - The service says
PORT is not set. Something other than service management started it. Start it withbarry service start counter. - Every
/apirequest returns 403. The request carries no secret, or the service doesn’t declareBARRY_SECRETundersecrets. - For anything else, see Troubleshooting.
What’s next
Section titled “What’s next”- Add a store to keep the service’s data between restarts.
- Services and stores for how service management works.
- The
/services/httpSDK reference and the/servicesSDK reference for every export.