---
title: "Link two apps on one machine"
description: "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."
section: "guides"
status: "Building for 0.4"
spec: "app-registry"
canonical: "https://mainsdk.dev/docs/guides/link-two-apps"
updated: "2026-10-28"
---

# 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.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/link-two-apps · 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](https://mainsdk.dev/docs/guides/seal-your-build)).
- 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

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

```yaml title="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

```ts title="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]);
```

```ts title="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

```yaml title="main.yaml (orders)"
neighbours:
  host: on
  asks: { accept: [status, load.shed] }
```

```ts title="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](https://mainsdk.dev/docs/interfaces/apps), [neighbours](https://mainsdk.dev/docs/interfaces/neighbours), [respond to an attack](https://mainsdk.dev/docs/guides/respond-to-attacks-and-pressure).
