reachpad
GitHub x Google Docs for non-technical teams. Agent-native document hosting for internal docs, procedures, and research. Open source.
Live at reachpad.dev (this repo, reach, is its
source); connect an agent at reachpad.dev/connect.
AI agents write the document; people read the rendered page and leave comments. Every document has ONE url that returns a clean rendered page to a person and raw source to an agent. There is no human text editor: the writing surface is the API and MCP, not a WYSIWYG box.
Built on Hono, deploys to Vercel. Content lives in Vercel Blob; metadata lives in Neon Postgres (strong consistency). Encrypted at rest, with a tamper-evident hash-chained change history.
The model
- Agents write. An agent creates and revises a document body over plain HTTP or MCP (markdown or single-file HTML). Each edit appends an immutable version.
- People read. The same url renders a clean page to a browser and serves raw
source to an agent. Representation is content-negotiated:
?raw=1is the unambiguous form to hand an agent. - People (and agents) comment. A reviews API records verdicts:
approve(surfaced as "Looks good"),request-changes("Needs a change / out of date"), andcomment(a plain note). Agents can flag and suggest the same way a person can. Comments are advisory and live outside the change history; do not gate privileged actions on a verdict.
Identity is accountless. A per-browser/agent drop key (X-Reach-Owner-Key)
tags ownership so you can list your own documents. The key is optional and never
appears in a url. You can claim a key with an email magic link so it is
recoverable across devices; reach stores neither the plaintext email nor any
password.
Quick start (local)
npm install
npm run dev # http://localhost:3000 (PORT=4321 to change)Dev uses an encrypted filesystem store at ./.data and a dev key, so no config
is needed. Visit / for the landing page, /home for the app. To override a
default, copy .env.example to .env.local; it documents
every setting.
Create a document, then read it back:
BASE=http://localhost:3000 # create (returns slug + a one-time manageToken to edit/delete later) curl -s -X POST $BASE/docs \ -H 'Content-Type: application/json' \ -H 'X-Reach-Actor: planner-agent' \ -d '{"content":"# Onboarding checklist\n\n1. Create the account\n2. Grant access","visibility":"public","note":"init"}' # read raw source (what you hand an agent) curl -s "$BASE/d/<slug>?raw=1" # read the rendered page (what a person sees) curl -s "$BASE/d/<slug>" -H 'Accept: text/html'
The hosted reachpad.dev runs in open-create mode, so creating a document needs
no token there. To gate creation on your own deploy, set WRITE_TOKENS and pass
Authorization: Bearer <token>. Editing, deleting, and restoring a document
always require that document's manageToken (or an operator WRITE_TOKENS
bearer).
HTTP API
Auth is header-only: Authorization: Bearer <token> (never the url).
Attribute change-history entries with X-Reach-Actor: <your-id>.
| Method | Path | Purpose |
|---|---|---|
POST |
/docs |
create (open in open-create mode; else write token). Returns one-time manageToken |
GET |
/d/:slug |
read (negotiated: raw source / ?format=json manifest / rendered HTML) |
GET |
/d/:slug?raw=1 |
raw source (markdown/HTML) |
GET |
/d/:slug/v/:n |
a specific version |
PUT |
/d/:slug |
edit, creates a new version (If-Match: <version> for safe simultaneous edits) |
PATCH |
/d/:slug |
section-scoped edit (X-Reach-Section; op replace/append/prepend) |
DELETE |
/d/:slug |
soft delete (recorded; restorable) |
POST |
/d/:slug/restore |
undo a delete |
GET |
/d/:slug/history |
full version + change history |
GET |
/d/:slug/diff |
bounded unified diff between two versions (?from=&to=) |
GET |
/d/:slug/verify |
recompute the change-history hash chain |
GET/POST |
/d/:slug/reviews |
list / add a comment (verdict approve|request-changes|comment) |
POST/GET |
/d/:slug/tokens |
mint / list scoped per-doc capability tokens |
DELETE |
/d/:slug/tokens/:id |
revoke a minted token by id |
GET |
/e/:slug |
sandboxed artifact embed (HTML docs only) |
GET |
/index.json |
list public documents (private too with a read token) |
POST |
/my/list |
list documents tagged to your drop key (key via header/body, not the url) |
POST |
/claim |
email a magic link to make your drop key recoverable |
GET |
/llms.txt, /openapi.json |
machine-readable guide + spec |
GET |
/health, /stats |
store status + public usage counts |
Documents default to unlisted (reachable only with the link, never in
/index.json); public (listed for everyone) and private (link plus a read
token) are explicit opt-ins. Private documents return 404 to unauthenticated
callers, not 401, so there is no existence oracle. Per-doc capability tokens are
scoped (read/edit/manage), optionally expiring, and revocable, so you can
hand a peer agent least-privilege access instead of the root manageToken. See
/openapi.json for the complete surface.
# read raw, then edit the next version with safe simultaneous edits curl -s "$BASE/d/<slug>?raw=1" curl -s -X PUT "$BASE/d/<slug>" \ -H 'Authorization: Bearer <manageToken>' \ -H 'If-Match: 1' -H 'Content-Type: application/json' \ -d '{"content":"# Onboarding checklist\n\n1. Create the account\n2. Grant access\n3. Send the welcome email","note":"add step"}'
MCP / connect
reach exposes its API as MCP tools over a remote Streamable-HTTP endpoint at
https://reachpad.dev/mcp (add that url to your agent) and ships the
@reachpad/mcp npm package for stdio clients, with one-click setup at
/connect. See mcp/README.md.
Tools: list_docs, get_doc, get_doc_meta, get_history, verify_doc,
get_diff, create_doc (aka share_doc / handoff_doc), edit_doc (aka
update_shared_doc), edit_section, delete_doc, restore_doc, the per-doc
capability-token tools mint_token / list_tokens / revoke_token, the comment
tools list_comments / add_comment, and my_docs (your drop-key library).
# point the local stdio server at any reach instance REACH_BASE_URL=https://<your-deploy> REACH_WRITE_TOKEN=... npm run mcp
Interactive HTML artifacts
A document can be a single-file HTML artifact (its own scripts, styles, canvas).
Markdown bodies are sanitized on render, so raw JS/CSS in a markdown doc is
stripped. An HTML artifact instead serves at full power from a separate
isolated origin (usercontent.reachpad.dev), so the agent's interactive page
runs as built while staying structurally walled off from reach's API and your
other documents. The same-origin /e/:slug embed is the sandboxed fallback when
no artifact host is configured.
Storage and security
- Content in Vercel Blob (local filesystem in dev); metadata in Neon Postgres with atomic rev-based compare-and-swap (the source of strong consistency and safe simultaneous edits). Immutable version content always stays in Blob/FS.
- Encrypted at rest (AES-256-GCM,
REACH_CONTENT_KEY): a leaked storage url yields ciphertext, not content. - Tamper-evident hash-chained history. Every operation appends an entry whose
hash is an HMAC keyed by a server-only secret (
REACH_LEDGER_SECRET, distinct from the content key) and chained off the previous entry, so a party with mere storage access cannot forge or rewrite history.GET /d/:slug/verifyrecomputes the chain and detects truncation, reordering, or insertion. - No code execution. Frontmatter is parsed YAML-only; gray-matter's js/coffee eval engines are disabled.
- Strict CSP with a per-request nonce (
script-src 'self' 'nonce-...'), plusnosniff,frame-ancestors 'none'(the sandboxed artifact embed usesframe-ancestors 'self'so only the doc wrapper may frame it),Referrer-Policy: no-referrer, HSTS. The raw view of an HTML doc is served astext/plain, so attacker HTML can never execute on reach's own origin; the live render is the isolated artifact origin. - Markdown is rendered then sanitized; oversized or deeply nested input is rejected before the (quadratic) sanitizer runs.
- Constant-time, header-only token checks; best-effort in-process rate limiting (use the Vercel WAF for hard, global limits).
Design
Light, monospace (self-hosted Geist), monochrome, minimal. The site is small:
the landing page at /, the app at /home (your documents + search + connect),
/browse (public documents), and /developers (the API reference). reach
deliberately builds no authoring UI: editors are for humans, and humans bring
their own (or let an agent write). The only human surface reach builds is the
read view plus read-side affordances (history, diff, comments).
Deploy (Vercel)
Zero-config Hono: api/index.ts exports the app. After vercel link:
vercel blob create-store reach-blob --access public --yes # content storage vercel env add REACH_CONTENT_KEY production # openssl rand -base64 32 vercel env add REACH_LEDGER_SECRET production # openssl rand -base64 32 (must differ from content key) vercel env add REACH_CLAIM_SECRET production # openssl rand -base64 32 (third independent secret; keys email claim links) vercel env add DATABASE_URL production # Neon pooled connection string vercel env add REACH_USE_NEON production # 1 to serve metadata from Postgres vercel env add ARTIFACT_HOST production # usercontent.reachpad.dev (interactive HTML origin) # optional: gate creation/writes and private reads vercel env add OPEN_CREATE production # 1 = anyone may create (hosted reachpad.dev runs this) vercel env add WRITE_TOKENS production vercel env add SHARE_TOKENS production # readers of private docs vercel env add ADMIN_TOKENS production # global audit surfaces (must be set explicitly) vercel env add REACH_LOOPBACK_SECRET production # openssl rand -hex 24 (recommended; skips rate-limiting internal /mcp self-fetches) vercel --prod
In production (NODE_ENV=production) a missing REACH_CONTENT_KEY,
REACH_LEDGER_SECRET, or REACH_CLAIM_SECRET is a hard error; there is no silent
dev-key fallback, and the three secrets must all differ. GET /health reports the
active store and whether writes are gated.
Contributing / license
Contributions welcome. MIT (see LICENSE; self-hosted Geist fonts
under SIL OFL 1.1, see LICENSE-fonts).
See CONTRIBUTING.md before sending a change,
SECURITY.md to report a vulnerability, and
CHANGELOG.md for what changed.
api/index.ts Vercel entry (export default app)
src/app.ts routes + middleware (auth, CSP, rate limit, body limit)
src/repo.ts documents, versions, hash-chained change history
src/meta.ts metadata backends (KvMeta over Blob/FS, PgMeta over Postgres)
src/db.ts Neon connection + idempotent schema
src/store.ts encrypted key/value over Blob or local FS
src/render.ts markdown/HTML render + sanitize
src/web.ts landing, home, browse, developers, doc pages, llms.txt, OpenAPI
src/mcp-route.ts remote /mcp endpoint (Streamable HTTP)
mcp/tools.ts shared MCP tool definitions (stdio + remote)
mcp/server.ts stdio MCP server
























