MainSDK
Search⌘K
How-to

Link two apps on one machine

List the apps on a machine, let two sealed apps prove who they are, call an action one exposes, and ask a neighbour for status or relief.

Building for 0.4

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will see the apps on your machine, link two of them, call an exposed action from one to the other, and ask a neighbour for its status. It takes about 25 minutes.

You need

  • Two MainSDK apps on one machine, run as the same user, each with a seal (see seal).
  • An identity key for each: an ed25519 key pair. The public half goes in apps.identity and the private half in the secret MAINSDK_APP_KEY.

1. See the apps on the machine

bash
npx mainsdk apps

You get one line per app with its owner, version and seal state, and any flags: a broken seal, a stale record, a duplicate project id.

2. Declare each other

main.yaml (orders)
apps:
  link: on
  identity: "ed25519:MCowBQYDK2VwAyEA…"
  peers: [prj_01JD8X]
  expose: [orders.status]
access:
  roles:
    app: [orders.status]
main.yaml (dashboard)
apps:
  link: on
  identity: "ed25519:MCowBQYDK2VwAyEA…"
  peers: [prj_01JC8Q]

Both sides must list each other, or a call fails with MAIN-APPS-0006.

3. Call an exposed action

orders
import { action, apps } from "mainsdk";

import { s } from "mainsdk/schema";

const ordersStatus = action({
  name: "orders.status",
  intent: "Read an order's status",
  input: s.object({ id: s.number() }),
  risk: "read",
  run: async ({ input }) => ({ id: input.id, status: "shipped" }),
});
apps.provide([ordersStatus]);
dashboard
import { apps } from "mainsdk";
const order = await apps.call("prj_01JC8Q", "orders.status", { input: { id: 1 } });

The orders app runs the action as a service named app:prj_01JD8X with the role app, so you grant it only what you mean to under access.roles.

4. Ask a neighbour

main.yaml (orders)
neighbours:
  host: on
  asks: { accept: [status, load.shed] }
dashboard
import { neighbours } from "mainsdk";
const s = await neighbours.ask("prj_01JC8Q", "status");
await neighbours.ask("prj_01JC8Q", "load.shed", { for: "10m", reason: "report run" });

The neighbour runs its slimming steps for at most asks.max-for and rolls them back by itself.

Check it

mainsdk check flags code that reads the registry folder by path (MAIN-APPS-0004) and files that mix peer data with an outbound call (MAIN-APPS-0005). In a test, createSim({ host: "sockets", apps: { a: { yaml, link: true } } }) runs the same wiring on the fake network.

Related: app registry and link, neighbours, respond to an attack.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet