Skip to content
WIP

This guide is a work in progress. Barry is also a work in progress.

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.

name: counter
description: 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.

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"
}
}

@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:

  1. applyBaseMiddleware sets 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 in BARRY_CORS_ORIGINS.
  2. Routes registered next stay public, so the health check goes here.
  3. applyAuth admits only requests that carry the instance secret, as a bearer token or an x-barry-secret header, 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 from BARRY_SECRET, so the service declares it.
  4. serve registers 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.
  5. listenOnAssignedPort listens on 127.0.0.1 at the port in PORT, and refuses to start without one.

Write diagnostics to stderr, and read them with barry service logs counter.web --stderr.

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.

Install the bag, which starts its services, then check the service:

Terminal
barry install ~/repos/counter
barry service status counter

The status shows counter.web running, with its port and a health of ok. To get its address, run:

Terminal
barry service url counter.web

A request to /health at that address returns {"ok":true}. A request to /api/count without the secret returns 403.

  • The status shows the service in backoff. The process keeps exiting. Run barry service run counter.web to 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 with barry service start counter.
  • Every /api request returns 403. The request carries no secret, or the service doesn’t declare BARRY_SECRET under secrets.
  • For anything else, see Troubleshooting.