---
name: reachpad
version: 2.4.2
description: Publish a page, a document, a file or a fetch handler to reachpad through its hosted MCP server, and get a link back. Use when something built in this session has to be openable by another person, when a link has to keep serving the version it was sent as, when access to it has to be limited to named people or to one organization, or when the thing being published has to keep data: a function app gets its own SQL database, a file store and the organization's secrets.
---

# reachpad

Reachpad publishes an app and serves it at a link. An app is a set of files:
an HTML page, a Markdown document, a file of any kind, or one JavaScript
module that exports `default { fetch }` and answers every request. Publishing
again makes a new version, and every version keeps a link of its own. A version
that fails to build keeps its number and stays in the history as failed, so the
next publish takes the number after it.

This file lives at https://reachpad.dev/SKILL.md. Asked to set it up, save it
to your skill directory (for example `.claude/skills/reachpad/SKILL.md`) so
it loads in future sessions, and re-fetch it when a saved copy and the product
disagree.

## Connect

The hosted MCP server is https://reachpad.dev/mcp
Streamable HTTP, POST only. It is an OAuth 2.1 resource server with dynamic
client registration, so there is no client id or secret to ask anybody for.
`initialize`, `ping` and `tools/list` answer with no credential;
`tools/call` needs the token.

- Claude Code: `claude mcp add --transport http --scope user reachpad https://reachpad.dev/mcp`, then `claude mcp login reachpad`.
- Codex: `codex mcp add reachpad --url https://reachpad.dev/mcp`, which starts the sign-in itself.
- Anything else: the URL above, or the REST routes under https://reachpad.dev/api/apps with the same bearer token.

How to get a credential: https://reachpad.dev/auth.md

## Publish

The tools below take plain text. If calling REST instead, set
`encoding: "utf8"` on each `{ path, content }` file. REST defaults to base64
when encoding is omitted; it does not infer text from a filename.

- `create_app` takes `name` and `files` (an array of `{ path, content }`,
  content written as plain text), and answers with the app id and the live
  URL. Version 1 is live when the call returns.
- `update_app` takes `app_id`, `files` and `base_version_id`, and replaces
  the whole file set with a new version. `mode: "merge"` sends only the files
  that changed and keeps the rest of the version `base_version_id` names,
  with `remove` for paths to drop. `validate: true` answers with what the call would publish
  and writes nothing. `publish: false` stages the version, and
  `publish_app_version` puts a staged or older version on the link.
- `manifest` says how a version runs. Omit it for a page, or pass
  `{ "kind": "function", "entry": "server.js" }` for a module that answers
  requests. `env` holds plain string values that module reads as
  `env.NAME`; never put a key in one. A function may also declare
  `services` and `secrets`, which is the Data section below.
- `set_app_access` takes `restricted`, `org_link` or `public_link`.
  `share_app` invites one email address as a `viewer` or an `editor`.

- `read_app` answers the app's files with each text file's content in them,
  up to 256 KB a file and 1 MB an answer, so read an app before editing it
  rather than rewriting it from memory. `read_app_file` fetches one file that
  was left out for its size.
- `query_app_db` runs one SQL statement against a function app's own
  database, for reading rows and fixing data. Schema changes are refused: a
  table or a column goes in the version's `migrations/NNNN_name.sql`.
- `list_secrets`, `set_secret` and `remove_secret` hold the
  organization's secrets, which a function binds by name in `manifest.secrets`.
  A value is sealed on write and no tool reads it back.
- `trash_app` stops an app's link. Restoring it, and deleting it for good,
  happen in Trash at https://reachpad.dev/apps/trash.

The rest of the surface: `search_apps`, `list_app_versions`,
`read_app_version`, `publish_app_version`, `list_app_shares`,
`revoke_app_share`, `get_app_logs`, and `ls`, `tree`, `mkdir`, `mv` and
`rmdir` for folders.

Arguments and limits in full: https://reachpad.dev/docs/publishing.md

## What a function is for

A function answers one request and returns one response. It is not a place to
run a job.

- **About 10 seconds of computing per request.** That counts
  time the code spends working, not time it spends waiting. Past it the request
  is stopped and whoever opened the link gets an error page.
- **90 seconds for the whole request.** The link stops
  waiting there.
- **Waiting is free.** Awaiting a query, a file read or a `fetch` to another
  service costs none of the 10, so calling something slow is
  fine inside the 90. A model call that streams its
  answer for a minute is an ordinary thing for a function to do.

Work that takes minutes, or that computes hard for more than a second or two --
rendering a video, resizing a large set of images, building a big export, a
long crawl -- does not go inside the handler. Start it on a service built to
run it, write a job row in the app's own database, answer with the id, and let
a later request report on it:

```js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === "/export" && request.method === "POST") {
      const id = crypto.randomUUID();
      await env.reachpad.db.query("INSERT INTO jobs (id, state) VALUES (?, 'running')", [id]);
      // Handing the work to something built to run it. Awaiting the handoff
      // costs no CPU; awaiting the WORK would cost all of it.
      await fetch(env.WORKER_URL, {
        method: "POST",
        headers: { authorization: `Bearer ${env.WORKER_TOKEN}` },
        body: JSON.stringify({ job: id }),
      });
      return Response.json({ job: id }, { status: 202 });
    }
    if (url.pathname.startsWith("/export/")) {
      const { rows } = await env.reachpad.db.query("SELECT state, result FROM jobs WHERE id = ?", [
        url.pathname.slice("/export/".length),
      ]);
      return rows.length ? Response.json(rows[0]) : new Response("no such job", { status: 404 });
    }
    return new Response("not found", { status: 404 });
  },
};
```

## Data

A function app can store data. A page cannot: it is files on a link with no
code to hold a credential. The manifest declares what the version uses, and
the front door hands the module `env.reachpad`:

```json
{ "kind": "function", "entry": "server.js",
  "services": ["db", "files"], "secrets": ["STRIPE_KEY"] }
```

- `services` takes `db`, `files` or both. A name outside that pair is
  refused at publish rather than at the first call.
- `env.reachpad.db.query(sql, params)` answers
  `{ rows, changes, lastInsertRowid }`, rows as objects keyed by column.
  `env.reachpad.db.batch([{ sql, params }])` runs 1 to 20 statements in one
  transaction and answers an array of the same object, one per statement, in
  the order sent: a statement that throws rolls the whole batch back.
- A parameter is a string, a number, a boolean, `null`, or
  `{ "$blob": "<base64>" }` for bytes, which is also the shape a BLOB column
  reads back in.
- One batch may return 5,000 rows or 4 MB, counted across every statement in
  it, and past either number the batch is refused. Write a `LIMIT`.
- One SQLite database per app, shared by every version of that app. There is
  no separate database for a staged version, and no Reachpad ceiling on its
  size: the 10 GB it stops at is the storage platform's own limit per
  database.
- `env.reachpad.files.put(id, body, contentType)` stores bytes under an id
  the app chooses, a lowercase UUID. `body` is a string, an ArrayBuffer, a
  view or a Blob, never a stream. `get(id)` answers a `Response` or
  `null`, `head(id)` answers `{ bytes, contentType, etag }` or `null`,
  `delete(id)` answers nothing. There is no list call: the app's own table
  is the index.
- 100 MB a file, and 1 GB and 10,000 files an app.
  `env.reachpad.usage()` answers `{ db_bytes, files, file_bytes }`.
- `request.reachpad` is `{ user, name, org, role, app, version }` for
  whoever opened the link. On a public link nobody signs in, so `user` and
  `name` are `null` and `role` is `anonymous`; `org` is the app's own
  organization either way. Read `role` before trusting `user`.
- Each name in `secrets` arrives as `env.NAME`. A secret is set once for
  the organization, at https://reachpad.dev/apps/settings/secrets or with
  `reachpad secrets set NAME`, and any app in that organization may bind it
  by name. Set it BEFORE publishing: a name the organization has not set fails
  the publish with `NAME is not set. Run reachpad secrets set NAME.`, and a
  name in both `env` and `secrets` fails with `NAME is both an env var and
  a secret. Keep one.`

Schema changes go in `migrations/NNNN_name.sql` beside the code: four or more
digits, an underscore, then lowercase letters, digits, `-` or `_`. They run
in ascending order when the version GOES LIVE, not when it is built, so a
version staged with `publish: false` does not change the schema under the
version still serving. Only a function declaring `db` may carry the folder,
at most 200 files of at most 100 KB each, and a migration file takes no bound
parameters. Never edit a file that has already been applied: the hash is
recorded, and a changed file is refused with
`changed after it was applied. Add a new file instead.` Each file is all or
nothing and a set is not, so a version carrying three files can stop at the
second, and the first one stays applied. A failing file fails the publish and the link keeps
serving the previous version; read the ledger, `_rp_migrations` in the app's
own database, to see what applied, fix that file, and publish again.

```js
export default {
  async fetch(request, env) {
    const { rows } = await env.reachpad.db.query(
      "SELECT id, title FROM notes WHERE owner = ? ORDER BY id DESC LIMIT 50",
      [request.reachpad.user],
    );
    return Response.json(rows);
  },
};
```

## Rules for agents

1. **Show the content and the destination before you publish.** `create_app`
   and `update_app` write to the app's real URL with no staging step and no
   draft unless you ask for one, so version 1 is live the moment the call
   returns. `validate: true` checks a call and writes nothing;
   `publish: false` on `update_app` holds the version back until
   `publish_app_version`.
2. **Read before you update.** Call `read_app` and pass the
   `base_version_id` it returns. A base that has moved is refused with
   `stale_base` rather than overwriting the version somebody else published.
   Read again and pass the `base_version_id` it returns.
3. **A publish replaces the whole file set.** A file you leave out of the call
   is not in the app any more. Send every file the app needs, every time, or
   pass `mode: "merge"` to send only the files you changed and keep the rest
   of the version `base_version_id` names.
4. **Say who can open it.** A new app defaults to `org_link`, which is
   everyone signed in at the person's own company. Anything narrower or wider
   is `set_app_access`, and it is worth naming out loud before publishing.
5. **Never delete a folder to tidy up.** `rmdir` refuses a folder that holds
   anything, and moving apps out to make it succeed moves somebody's work.
6. **Hand over an old version's link, not a rebuild.** Version 2 of `todo`
   answers at `https://todo--preview.reachpad.app/_rp/preview?version=2` for as long as the app exists.
   Earlier versions open for the app's editors; the live link follows the app's
   access setting.
7. **Do not run a job inside a request.** A function has about
   10 seconds of computing and 90
   seconds overall. Anything longer -- a render, a big export, a crawl -- goes to
   a service built to run it, with a job row in the app's database and an id in
   the answer. Waiting on another service costs none of that computing budget;
   doing the work in the handler costs all of it.
