A self-hosted read-it-later app built on Cloudflare's free tier. Save bookmarks from a Chrome extension or Android share sheet, search them semantically or by tag, and browse them from a web dashboard.
git clone https://github.com/mlashcorp/stash.git
cd stash
./scripts/setup.shThe script walks you through every step: creates the D1 database, Vectorize index, Cloudflare Access application (via Terraform), sets all secrets, and deploys.
Prerequisites: Node.js 18+, Wrangler authenticated (wrangler login), OpenRouter API key. Terraform ≥ 1.7 is optional but automates the Cloudflare Access configuration.
For full control over each step, see Manual Setup.
- Worker — Hono on Cloudflare Workers (TypeScript)
- Database — Cloudflare D1 (SQLite)
- Vector search — Cloudflare Vectorize
- AI tagging + embeddings — OpenRouter (Gemini Flash Lite)
- Auth — Cloudflare Access (browser + extension)
- Extension — Chrome MV3
Without this, your dashboard is publicly accessible. Cloudflare Access puts an identity gate in front of it — only you can log in.
The setup script handles the Terraform-managed part automatically. There is one step that must be done manually from the Cloudflare dashboard — it cannot be done via API or Terraform:
- Go to Cloudflare Dashboard → Workers & Pages → select your
stashWorker - Go to Settings → Domains & Routes
- Click Enable Cloudflare Access on the
workers.devrow - Click Manage Cloudflare Access → create a policy (e.g. allow your email + One-Time PIN)
The setup script will pause and prompt you to complete this step before continuing.
See Manual Setup for the full breakdown including the Terraform steps and how to configure everything without the script.
The extension authenticates using the same Cloudflare Access session as the dashboard — no API token needed.
- Open Chrome →
chrome://extensions - Enable Developer mode
- Click Load unpacked → select the
extension/folder - Click the extension icon → open Settings → enter your Worker URL → Save
- Make sure you're logged into your Stash dashboard in the same browser
After that, click the extension icon on any page to save it to your Stash in one click.
On Android, Stash can appear in the native share sheet — letting you save any link from any app with one tap.
- Open your Stash URL in Chrome for Android
- Tap the three-dot menu → Add to Home screen → Install
- Stash will appear on your home screen and in the share sheet
If saving fails, open the dashboard directly to refresh your session, then try again.
Stash ships with an MCP server so AI assistants (Claude, etc.) can save, search, and manage your bookmarks directly.
- Create an API key in the Stash dashboard (Settings → API Keys)
- Add to your Claude Desktop config (
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS):
{
"mcpServers": {
"stash": {
"command": "npx",
"args": ["-y", "stash-mcp-server"],
"env": {
"STASH_URL": "https://stash.<your-subdomain>.workers.dev",
"STASH_API_KEY": "stash_your_key_here"
}
}
}
}Restart Claude Desktop — the stash tools will appear automatically.
| Tool | Description |
|---|---|
save_bookmark |
Save a URL with title; returns ID and AI-generated tags |
delete_bookmark |
Delete a bookmark by ID |
add_tag / remove_tag |
Manage tags on a bookmark |
list_tags |
List all tags with counts |
search_bookmarks |
Fuzzy, semantic, or tag-filter search |
suggest_titles |
Autocomplete bookmark titles |
The MCP server source lives in mcp-server/. See mcp-server/README.md for more details.
For programmatic access (scripts, the MCP server), you can create API keys from the settings page (gear icon in the header). Keys are shown once on creation — store them somewhere safe.
All API requests with a key use the X-API-Token header:
curl https://stash.<your-subdomain>.workers.dev/api/save \
-H "X-API-Token: stash_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "title": "Example"}'| Method | Path | Description |
|---|---|---|
POST |
/api/save |
Save a bookmark { url, title } |
DELETE |
/api/save/:id |
Delete a bookmark |
GET |
/api/tags |
List all tags with counts |
GET |
/api/search?mode=fuzzy&q=...&page=N |
Fuzzy search (paginated) |
GET |
/api/search?mode=tag&tag=...&page=N |
Filter by tag (paginated) |
GET |
/api/search?mode=semantic&q=... |
Semantic vector search |
GET |
/api/keys |
List API keys |
POST |
/api/keys |
Create an API key { name } |
DELETE |
/api/keys/:id |
Revoke an API key |
npm test # Worker unit tests (Vitest)
npm run test:scripts # Bash script tests (no dependencies)
npm run test:terraform # Terraform tests using mock_provider (requires Terraform >= 1.7)The Worker tests run against a local Cloudflare Workers runtime via Vitest. The script tests cover wrangler::patch_wrangler_toml and the terraform.tfvars placeholder detection — both operate on temp files with no external calls. The Terraform tests use mock_provider "cloudflare" {} so no credentials or real resources are needed.
Copy .dev.vars.example to .dev.vars and fill in your values — Wrangler loads this automatically during wrangler dev.
npm run devD1 and Vectorize work locally via Wrangler's local simulation. Run wrangler d1 migrations apply stash-db --local first to set up the local database.
MIT