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
Section titled “Worker”"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.
A worker, step by step
Section titled “A worker, step by step”matter app create --worker sets all of this up. To add a worker to a project yourself:
-
Write it, in TypeScript if you like (
src/worker.ts). Import the SDK fromMATTER_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.
-
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"} -
Name it in
matter.json, relative todeploy.dir, and givematter app devits 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.
Every member runs it
Section titled “Every member runs it”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
Section titled “Run server”"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 environment
Section titled “The environment”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:
eval "$(matter app env)"node dist/worker.mjsEach process’s output goes to a log Matter keeps for the app on this computer:
matter app logs # the end of each logmatter app logs worker -f # the worker's, as it writesmatter app logs run -n 200 # the run server's last 200 linesmatter app logs dev-worker -f # your dev worker's, in matter app devThe 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.
When a process stops
Section titled “When a process stops”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.