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.
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=1
is 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.
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
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"}'
curl -s "$BASE/d/<slug>?raw=1"
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).
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.
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"}'
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).
REACH_BASE_URL=https://<your-deploy> REACH_WRITE_TOKEN=... npm run mcp
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.
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/verify
recomputes 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).
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).
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)
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.
Contributions welcome. MIT (see LICENSE; self-hosted Geist fonts under SIL OFL 1.1, see
LICENSE-fonts
See CONTRIBUTING.md before sending a change,
to report a vulnerability, and
SECURITY.md
CHANGELOG.md
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