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

Otter is released under the MIT license. PRs welcome! Follow @zander

Features • Packages • Getting started • Tech stack

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 with corepack 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:

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:

  1. Installs dependencies
  2. Runs semantic-release inside each releasable package via pnpm --filter ... exec semantic-release
  3. For each package with relevant new commits: bumps package.json, updates that package's CHANGELOG.md, commits the bump back to main, 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

MIT © Zander Martineau

Made by Zander • zander.wtf • GitHub • Mastodon