Skip to content

Worker, tools and run server

An app can run code on the member’s computer in three ways. Each is a JavaScript entry file in the deployed folder, named in matter.json. Matter runs them with its own Node, so the member doesn’t need Node installed, and as the member. They run the same on macOS, Windows and Linux.

Bundle each entry file with what it imports (with esbuild, for example), since the deployment has no node_modules unless you deploy one.

"worker": "worker.mjs" is a long-running process. It starts on a member’s computer when they first open the app there, and keeps running, across Matter restarts, until they choose Stop in the app’s menu. It’s restarted with each new version of the app.

Use it for work that should happen as data changes, without an agent: sending what someone approved, syncing with a service, tidying up. Watch the tables it cares about with live reads.

matter app create --worker sets all of this up. To add a worker to a project yourself:

  1. Write it, in TypeScript if you like (src/worker.ts). Import the SDK from MATTER_SDK_URL:

    const { default: matter } = (await import(process.env.MATTER_SDK_URL!)) as typeof import('/_matter/sdk.js')
    const context = await matter.context()
    console.log(`Worker started for ${context.user.name}`)
    matter
    .table('Actions')
    .watchIds({ index: 'by_sender_status', eq: { Sender: '$viewer', Status: 'approved' } }, (state) => {
    if (state.status === 'ready') for (const id of state.value.ids) void send(id)
    })

    A worker runs until Matter stops it, and one that exits is started again. Its live reads keep it running.

  2. Bundle it into the deployed folder, after the pages are built:

    "scripts": {
    "build": "tsc --noEmit && vite build && npm run build:worker",
    "build:worker": "esbuild src/worker.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/worker.mjs",
    "dev:worker": "tsx watch src/worker.ts"
    }
  3. Name it in matter.json, relative to deploy.dir, and give matter app dev its dev command:

    "worker": "worker.mjs",
    "dev": { "command": "npm run dev", "url": "http://localhost:5173", "worker": "npm run dev:worker" }

matter app dev then runs the worker from your project as you change it, as your dev worker, and the deployed one keeps running alongside it. See Developing.

Each member who has the app open runs its worker, and in dev your dev worker runs beside your live one. So two workers may act on the same row. Make what each does safe to run twice: claim work with a conditional update, which only one of them wins, before acting on it. Work that should happen once for the whole app suits an app routine better.

"tools": "tools.mjs" is an MCP server, on stdio. The app’s routine runs, and threads the app starts, get it, so agents can do the app’s work through operations you define, instead of editing its tables directly. Matter starts it for each run that uses it.

Write tools that check and move state safely, for example with conditional updates, so two runs can’t take the same work. Any MCP server library works. The tools can call whatever they like, using the member’s own settings.

A routine run works in a folder of its own, not in the app’s files, so open files relative to your module (new URL('./prompts/x.md', import.meta.url)), not the working directory. If the member doesn’t trust the app’s key on their computer, its routine runs there stop at once.

"run": "server.mjs" is a local HTTP server behind the app’s pages. With it, Matter forwards every page request, other than /_matter/*, to your server instead of serving the deployed files. Listen on PORT, on HOST (the loopback address, 127.0.0.1): nothing else on the computer, or the network, should reach it.

Matter tells your server who it’s serving, in headers it adds to each request and signs with MATTER_RUN_SECRET, a secret of this launch:

Header What it is
x-matter-member The member’s id.
x-matter-app The app’s id.
x-matter-deployment The version’s id.
x-matter-context The context, as base64url JSON.
x-matter-timestamp When Matter forwarded it.
x-matter-signature The signature over the request and these headers.

Matter strips any x-matter-* headers that come in from outside, and its own cookie and credentials. Check every request with matter.verifyRequest, which takes a Node request or a fetch Request. It resolves to who’s asking, or null for a request Matter didn’t sign, or signed more than five minutes ago:

import { createServer } from 'node:http'
const { default: matter } = await import(process.env.MATTER_SDK_URL)
createServer(async (req, res) => {
const who = await matter.verifyRequest(req)
if (!who) return res.writeHead(403).end()
res.end(`Hello ${who.context.user.name}`)
}).listen(Number(process.env.PORT), process.env.HOST)

A page asked for while the server starts waits up to ten seconds for it to listen.

The worker, tools and run server get:

Variable What it is
MATTER_API_URL, MATTER_APP_TOKEN Where and how to reach the data API. The SDK uses them.
MATTER_SDK_URL The SDK module, as a URL to import() (on every OS).
MATTER_SDK The same module, as a path.
MATTER_APP_ID, MATTER_MEMBER_ID, MATTER_WORKSPACE_ID The app, the member and the workspace.
MATTER_DEPLOYMENT_ID The version running, or dev in matter app dev.
MATTER_DATA_DIR A folder of the app’s own on this computer, kept across versions.
MATTER_APP_PROCESS worker, run or tools; dev-server or dev-worker in dev.
PORT, HOST Where to listen (run server only).
MATTER_RUN_SECRET What matter.verifyRequest checks with (run server, dev server).
MATTER_THREAD_ID The thread the tools serve (tools only).

The worker’s and run server’s working directory is the deployed folder. Treat it as read-only, and keep files you write in MATTER_DATA_DIR. In dev, MATTER_DATA_DIR is a folder of dev’s own, so your dev worker doesn’t share the live one’s data.

To run one by hand against your workspace’s data:

Terminal window
eval "$(matter app env)"
node dist/worker.mjs

Each process’s output goes to a log Matter keeps for the app on this computer:

Terminal window
matter app logs # the end of each log
matter app logs worker -f # the worker's, as it writes
matter app logs run -n 200 # the run server's last 200 lines
matter app logs dev-worker -f # your dev worker's, in matter app dev

The logs are worker, run, dev-server and dev-worker; --preview shows a preview’s. A log over 10 MB is set aside, and a new one started.

If the worker or run server exits, Matter restarts it, waiting a little longer each time, up to a minute. After five exits in ten minutes it stops trying. A new version starts it again, as does choosing Stop in the app’s menu and then opening the app. Stopping a process stops whatever it started, too.