Otter
A self-hosted bookmark manager and media tracker built for people who value privacy and ownership.
Otter is my labour of love. This is a self‑hosted bookmark manager that I have been building for the past five years. It started on the web as a SvelteKit project, then I transitioned to Next.js. Now it runs as an SPA on TanStack Router. The Postgres database is hosted on Neon (external site).
The iOS app is where a lot of the development activity is going on at the moment. It has a whole bunch of features that the web app does not have. It includes read‑later functionality and an RSS reader as well as bookmarking.
There’s also a Raycast extension and Chrome and Firefox extensions, as well a TUI so you can access it on the command line. You name it, Otter’s probably got it. Read my Otter ecosystem post to find out more.
Project README

Otter
Otter is a self-hosted bookmark manager and media tracker built with React, Hono, Postgres, and Cloudflare Workers
Features
- Private bookmarking app with search, tagging, collections, and filtering
- Starred items and public/private visibility per bookmark
- Dark/light colour modes
- Media tracking — kanban-style board for tracking movies, TV shows, games, and more
- AI-powered title and description rewriting via Cloudflare Workers AI
- RSS feed parsing and URL scraping
- Mastodon integration — backup your own toots and favourite toots, plus auto-posting public bookmarks
- Bluesky and Twitter/X import
- MCP server — let MCP clients (Claude, Cursor, etc.) search and manage your bookmarks
- REST API with API-key auth, plus an OAuth provider for first-party clients
- Cross-browser web extension (Chrome & Firefox)
- Raycast extension to search, view, and create bookmarks
- Terminal UI (
otter-tui) - Native macOS/iOS app
- Bookmarklet
Screenshots
Feed (dark mode) ![]() |
Feed (light mode) ![]() |
|---|---|
New bookmark ![]() |
Search ![]() |
Feed (showing tags sidebar) ![]() |
Toots feed ![]() |
Packages
This is a pnpm monorepo containing the following packages:
| Package | Description |
|---|---|
packages/web |
Web app, Hono API, OAuth + MCP provider on Cloudflare Workers |
packages/app |
Native macOS/iOS app (Safari web extension + share sheet) |
packages/web-extension |
Cross-browser extension (Chrome & Firefox) |
packages/raycast-extension |
Raycast extension |
packages/tui |
Terminal UI — browse, search, star and save bookmarks from the shell |
packages/chrome-extension |
Legacy Chrome extension (superseded by web-extension) |
Getting started
Prerequisites
- pnpm v11 (pinned via
packageManager) — install withcorepack enable && corepack prepare pnpm@latest --activate - A Postgres database, e.g. Neon
- Cloudflare account — used for hosting, Workers AI, Hyperdrive, and the API
For a full walkthrough — including database setup, Cloudflare configuration, and deployment — see the Setup Instructions.
Quick start
pnpm install
pnpm web:dev
Releasing
Otter uses semantic-release with the semantic-release-monorepo plugin to version each package independently based on Conventional Commits. Packages are not published to npm — releases are GitHub releases only.
Commit message format
| Prefix | Release type |
|---|---|
fix: |
Patch (1.0.x) |
feat: |
Minor (1.x.0) |
feat!: or BREAKING CHANGE: |
Major (x.0.0) |
Per-package versioning
Each releasable package has its own release.config.mjs that extends semantic-release-monorepo. The plugin filters commits to those that touch files inside the package's directory, so a commit changing only packages/web will only bump and release @mrmartineau/otter-web.
Releasable packages:
@mrmartineau/otter-web— tags as@mrmartineau/[email protected]@mrmartineau/otter-chrome-extension— tags as@mrmartineau/[email protected]@mrmartineau/otter-web-extension— tags as@mrmartineau/[email protected]@mrmartineau/otter-tui— tags as@mrmartineau/[email protected]
The app and raycast-extension packages are released through their own platforms (App Store / Raycast Store) and are not part of this workflow.
CI / automated releases
Releases are triggered manually via the "Release" workflow in GitHub Actions (.github/workflows/release.yml). The workflow:
- Installs dependencies
- Runs
semantic-releaseinside each releasable package viapnpm --filter ... exec semantic-release - For each package with relevant new commits: bumps
package.json, updates that package'sCHANGELOG.md, commits the bump back tomain, and creates a GitHub release with package-scoped tag and notes
GITHUB_TOKEN is provided automatically by GitHub Actions — no additional secrets required.
Scoping commits
To target a specific package, use a Conventional Commits scope, e.g. feat(web): ... or fix(chrome-extension): .... The plugin uses changed file paths (not the scope) to decide which package releases, but scopes make the changelog clearer.
Tech stack
- Frontend: React 19, TanStack Router, TanStack Query, Tailwind CSS v4, Radix UI
- API: Hono on Cloudflare Workers with AI bindings
- Database: Postgres (e.g. Neon) via Drizzle ORM and Cloudflare Hyperdrive
- Auth: Better Auth with OAuth provider + JWT plugins
- Hosting: Cloudflare
- Tooling: pnpm workspaces, Biome (formatting & linting), Vite, Vitest
License
Made by Zander • zander.wtf • GitHub • Mastodon





