The agent-native way to share HTML, Markdown, and PDF documents.
Drop a file, get a short, optionally password-protected link.
Built for AI agents and humans — no hosting infrastructure required.
You generate HTML reports with Claude. Markdown writeups with Cursor. PDFs from a hundred different tools. Sharing them means either pasting raw content into chat (loses fidelity, can't password-protect), spinning up your own Vercel/Netlify project (slow, overkill for a single doc), or using something like Tiiny.host (works, but not built for AI workflows).
echo is the third option — purpose-built for the "I just generated this, give me a link" workflow, with a first-class agent surface (SKILL.md) so any agent runtime can publish without an MCP server.
| Feature | What it does |
|---|---|
| Drop or paste | Drag .md, .html, or .pdf files into the homepage, or paste raw Markdown/HTML |
| Markdown rendering | Sanitized server-render in a paper-style card with a sidebar TOC and heading anchors |
| HTML rendering | Runs inside a sandboxed iframe (sandbox="allow-scripts") — scripts can't escape to the parent origin |
| PDF rendering | Streams to the browser's native PDF viewer via a dedicated bytes route |
| Password gate | bcrypt-hashed passwords, HMAC-signed unlock cookies, per-doc 24-hour expiry |
| SEO opt-in | Per-doc indexable flag — default noindex since docs are share-by-link |
SKILL.md endpoint |
Drop-in instructions at /skill.md so Claude, Cursor, and other agents can publish without an MCP server |
flowchart LR
subgraph Publish
Form[Web form] -->|POST| API[/api/publish/]
Agent[Agent + SKILL.md] -->|POST| API
API --> DB[(Supabase<br/>Postgres)]
end
subgraph View
Visitor[Visitor] -->|GET| Route[/d/slug/]
Route --> DB
Route --> Fork{format?}
Fork -->|md| MD[Sanitized HTML<br/>+ TOC sidebar]
Fork -->|html| HTML[Sandboxed<br/>iframe]
Fork -->|pdf| PDF[Native PDF<br/>viewer]
end
- Next.js 15 (App Router) on Vercel
- Supabase Postgres for storage
- marked + sanitize-html for Markdown
- bcryptjs + Node
cryptoHMAC for password gating - Vercel Analytics + Speed Insights
git clone https://github.com/caglarbozkurt/echo.git
cd echo
npm installSign up at supabase.com and create a new project (free tier is enough).
- Open SQL Editor → paste the contents of
db/schema.sql→ run - Settings → API → copy the Project URL and secret key (
sb_secret_…)
cp .env.example .env.localFill in .env.local:
SUPABASE_URL=https://<your-project>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=sb_secret_…
ECHO_COOKIE_SECRET=… # openssl rand -hex 32
NEXT_PUBLIC_BASE_URL=http://localhost:3000npm run devOpen http://localhost:3000.
- Push your fork to GitHub
- vercel.com → Add New → Project → import the repo
- Add the same four env vars under Settings → Environment Variables
- Deploy. After the first deploy, set
NEXT_PUBLIC_BASE_URLto your assigned production URL and redeploy once so the API responses andSKILL.mdreference the correct host
POST /api/publish accepts JSON. No authentication required — same risk profile as the web form (also open).
curl -X POST https://<your-domain>/api/publish \
-H "Content-Type: application/json" \
-d '{
"content": "# Hello",
"format": "md",
"password": "optional",
"title": "optional",
"indexable": false
}'
# → { "slug": "abc123xy", "url": "https://<your-domain>/d/abc123xy" }PDFs are sent base64-encoded:
curl -X POST https://<your-domain>/api/publish \
-H "Content-Type: application/json" \
-d "{
\"content\": \"$(base64 -i my.pdf)\",
\"format\": \"pdf\",
\"title\": \"My PDF\"
}"Full parameter reference at /skill.md.
SKILL.md is served dynamically at /skill.md with the production URL substituted in. It follows Anthropic's skill format, so any agent runtime that reads skills will pick it up:
mkdir -p ~/.claude/skills/echo
curl -sS https://echo-kappa-peach.vercel.app/skill.md \
-o ~/.claude/skills/echo/SKILL.mdIn a new session, asking "publish this writeup to echo" will auto-trigger the skill — Claude will call /api/publish and return the share URL.
Click to expand the file tree
src/
├── app/
│ ├── page.tsx # Homepage: form, agent callout, log feed
│ ├── layout.tsx # Metadata, OG tags, Analytics
│ ├── actions.ts # Server action for the web form
│ ├── api/publish/route.ts # JSON publish endpoint
│ ├── d/[slug]/
│ │ ├── page.tsx # Render (md article / html iframe / pdf iframe)
│ │ └── pdf/route.ts # PDF byte stream (Content-Type: application/pdf)
│ ├── published/[slug]/page.tsx
│ └── skill.md/route.ts # Dynamic SKILL.md with host substitution
├── components/
│ ├── PublishForm.tsx # Client: tabs, dropzone, format detection
│ ├── BrandHeader.tsx # Sticky masthead with copy-link
│ ├── TableOfContents.tsx # MD doc sidebar
│ ├── CopyButton.tsx
│ └── Footer.tsx
├── lib/
│ ├── supabase.ts # Server-only client (service role)
│ ├── db.ts # getDocBySlug / insertDoc
│ ├── auth.ts # bcrypt + HMAC unlock tokens
│ ├── markdown.ts # marked + sanitize-html, heading IDs, TOC
│ └── baseUrl.ts
└── config/log.ts # Curated homepage log feed
db/
├── schema.sql # Single `documents` table
└── migrations/ # Idempotent ALTER scripts
public/
├── example.md # The "what is echo" doc
└── robots.txt # /d/ crawlable, per-page noindex by default
Issues, feature ideas, and small PRs are welcome. For anything non-trivial, please open an issue first. See CONTRIBUTING.md for the full guidelines.
MIT — built by Caglar Bozkurt.