bb is a bookmark manager for people who like to collect shit only to never revisit it again. It comes with a full-featured web UI and a CLI, supports image previews, and automatically scrapes page metadata. Run it as a standalone CLI tool or deploy as a daemon on a remote server.
This project is heavily work-in-progress!
bb is inspired by buku.
- Tags: Categorize bookmarks with tags. Tags are hierarchical — use
/to create nested categories (e.g.dev/rust,dev/python). Searching for a parent tag matches all children: filtering bydevalso matchesdev/rustanddev/python. This applies to both tag filters and the#query prefix. - Rules: Create custom rules using YAML configuration. Define matching queries for titles, URLs, or descriptions, and apply actions based on those matches. Rules also support an optional
querycondition — a search query string evaluated via the search query language (supports#tag,.title,>desc,:url,=id,and,or,not, quoted phrases, parens). The query condition is AND-ed with other conditions. For example, bb can automatically assign tag "dev" for every url containing "github.com", or usequery: "#work and :github.com"to match bookmarks tagged "work" with GitHub URLs. Rules can be managed via the Web UI Settings panel or the CLI. - Scrape Metadata: When you create a bookmark, bb fetches metadata through a multi-stage pipeline: URLs are normalized (tracking params stripped, hosts lowercased), then bb fans out parallel requests to oEmbed, Plain HTML, Microlink, and Peekalink fetchers. Results are merged field-by-field by priority. Images are validated via magic byte detection, decode check, and minimum resolution (>32x32) to filter out tracking pixels and corrupt data. Headless Chrome is used as fallback when no validated image is found. The Chrome instance includes fingerprint spoofing (deviceMemory, maxTouchPoints, WebGL vendor/renderer, AudioContext) to bypass bot detection. Failed metadata tasks are retried up to 3 times (configurable) with exponential backoff (5s × 2^attempt + jitter) for transient errors (5xx, timeout); 4xx errors are terminal. You can also upload custom cover images and favicons per bookmark via the Web UI.
- Web UI: Manage your bookmarks through a user-friendly web interface built with Vite, React, and shadcn/ui. Stores screenshots and favicons for quick reference. Installable as a PWA with share target and protocol handler support — share URLs directly from your browser or OS into bb.
- Workspaces: Organize bookmarks into filtered views. Each workspace defines tag whitelist/blacklist and an optional filter query. Bookmarks matching the workspace filters appear automatically. Workspaces are persisted in
workspaces.yamland managed via the Web UI settings panel or the REST API. Drag-and-drop reordering is supported. - Bulk Operations: Edit or delete multiple bookmarks at once. Bulk actions apply to all bookmarks matching the current search query — add, remove, or overwrite tags, and update fields in batch. Available from the toolbar in the Web UI.
- Editor Mode: Use
bb add --editorto open your$EDITORwith a structured template for filling in URL, title, tags, and description. Leave a field as-to skip auto-fill for that field. Works with any editor — vim, nvim, nano, etc. - Standalone CLI Tool or Daemon: Run bb as a standalone CLI tool or deploy it as a daemon on a remote server. Use the bb-cli as a lightweight client to connect to the server over HTTP.
- Semantic Search (experimental): Find bookmarks by meaning rather than exact text matches. Currently inaccurate for most workloads — prefer text search for reliable results. Disabled by default.
The query field supports a structured query language with field prefixes, boolean operators, quoted phrases, and grouping.
| Prefix | Field | Example |
|---|---|---|
# |
tags | #video — exact tag match + hierarchical (#programming matches programming/rust) |
. |
title | .youtube — substring, case-insensitive |
> |
description | >tutorial — substring, case-insensitive |
: |
url | :github.com — substring, case-insensitive |
= |
id | =42 — exact bookmark id match |
| (none) | all fields | video — substring across title, description, url, tags |
Group multiple words into a single term: ."getting started", >"deploy with docker", "async runtime".
and— both must match (implicit when terms are space-separated)or— either must matchnot— negates the following term/group- Precedence:
not>and>or - To search reserved words literally, quote them:
"and","or","not"
Group sub-expressions: (#video and .youtube) or (#audio and .spotify)
Search prefix characters literally: \#hashtag, \:colon, \.dot, \>arrow, \=id
rust async → bookmarks mentioning both "rust" and "async" anywhere
#recipe not #dessert → tagged "recipe" but not "dessert"
."getting started" → title contains the phrase "getting started"
>"deploy to production" → description contains "deploy to production"
:stackoverflow.com → URL contains "stackoverflow.com"
=42 → bookmark with id 42
#dev/backend or #dev/frontend → hierarchical tag match on either subtree
(#python or #rust) .tutorial → tutorials tagged python or rust
not #read :arxiv.org → unread papers from arxiv
- Paste a URL anywhere to instantly open the create bookmark modal with the URL pre-filled (as long as focus isn't in a text field).
- Ctrl+N opens the create bookmark modal.
- Ctrl+Enter saves edits in the detail modal.
- Normal view shortcuts:
ifocuses search,,opens settings,wfocuses workspaces,stoggles advanced filters,ppins the current query,atoggles Show All,vcycles grid/list/table. - Keyboard navigation: arrow keys or
h/j/k/lstart from the first visible bookmark, then move the current selection;eedits the selected bookmark andddeletes it with a double press. - Detail modal shortcuts:
eenters edit mode,ddeletes with a double press,rrefreshes metadata with a double press, andh/lor left/right navigate between bookmarks. - Click any tag to add it as a search filter.
- Double-click the delete button to confirm deletion (first click arms, second executes).
- Enter/Escape blur search fields so you can drop back to keyboard navigation quickly.
- Drag & drop or paste images onto the cover or favicon area in edit mode to upload custom images.
- URL parameters are persisted — any query parameters in the URL are applied on load. Supported params:
workspace,query,tags,title,url,description,semantic,all. For example,/?workspace=Dev&query=rust&tags=langopens the Dev workspace with "rust" in the query field and "lang" as a tag filter. Bookmark this URL to always start with a specific view. ?action=create— opens the create bookmark modal on page load. Combine with other params to pre-fill fields:/?action=create&url=https://example.com&title=Example&tags=readingopens the modal with those values already populated.
The Web UI settings panel (gear icon) has two sections:
-
Preferences
- Show catch-all workspace — display a "---" option in the workspace selector to view all bookmarks regardless of workspace.
- Show tag autocomplete — toggles visibility of tag suggestion dropdowns while typing. Keyboard autocomplete (Enter/Tab) still works even when hidden.
- Autocomplete substring match — when enabled, tag autocomplete matches anywhere in the tag. Disabled by default, so autocomplete uses group/tag prefix matching.
- Polling intervals — configure how often the UI refreshes data in the background (separate intervals for normal, busy, and hidden-tab states).
- Globally ignored tags — bookmarks with these tags are completely hidden everywhere in the UI. Useful for archiving or soft-deleting without removing data.
-
Workspaces — create, edit, reorder, and delete workspaces. Each workspace defines tag whitelists/blacklists and an optional filter query.
bb includes local semantic search powered by fastembed and ONNX models. Embeddings are generated locally—no external API calls.
-
Enable in config (
~/.local/share/bb/config.yaml):semantic_search: enabled: true
-
Search by meaning:
# CLI bb search --sem "machine learning tutorials" # With similarity threshold (0.0-1.0, default 0.35) bb search --sem "web development" --threshold 0.5
- When you create or update a bookmark, bb generates an embedding from the title, description, tags, and URL domain
- Embeddings are stored in
vectors.binalongside your bookmarks - Hybrid search combines two ranking methods:
- Semantic ranking — cosine similarity between query and bookmark embeddings
- Lexical ranking — text matching against title, description, and tags
- Results are merged using Reciprocal Rank Fusion (RRF) — items appearing in both rankings get boosted
- Other search criteria (tags, title filters) are applied before semantic ranking
semantic_search:
enabled: true # Enable/disable semantic search
model: "all-MiniLM-L6-v2" # Embedding model (default, ~23MB)
default_threshold: 0.35 # Minimum similarity score (0.0-1.0)
download_timeout_secs: 300 # Model download timeout
semantic_weight: 0.6 # Balance between semantic and lexical rankingAvailable models: all-MiniLM-L6-v2 (default), bge-small-en-v1.5, bge-base-en-v1.5, bge-large-en-v1.5 (and quantized variants).
- First search downloads the model (~23MB for default)
- Models cached at
~/.local/share/bb/models/ - Combine with other filters:
bb search --sem "tutorials" --tags dev - Web UI shows semantic input when feature is enabled
Controls URL fetching behavior for metadata scraping:
scrape:
# Accept invalid TLS certificates (default: false)
accept_invalid_certs: false
# Allowed URL schemes for fetching (default: ["http", "https"])
allowed_schemes:
- http
- https
# Blocked hostnames (default: [])
blocked_hosts: []
# Block requests to private/loopback IP ranges (default: true)
# Prevents SSRF attacks by rejecting 127.0.0.1, 192.168.x.x, etc.
block_private_ips: true
# Maximum retries for failed metadata fetches (default: 3, range: 1-10)
# Only transient errors (5xx, timeout, connection) are retried with exponential backoff
task_queue_max_retries: 3Configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
scrape.accept_invalid_certs |
bool | false |
Accept invalid TLS certificates when fetching URLs |
scrape.allowed_schemes |
list | ["http", "https"] |
Allowed URL schemes (e.g., http, https, ftp) |
scrape.blocked_hosts |
list | [] |
Blocked hostnames — requests to these hosts will be rejected |
scrape.block_private_ips |
bool | true |
Block requests to private/loopback IP ranges for SSRF protection |
task_queue_max_retries |
int | 3 |
Max retries for transient metadata fetch failures (5xx, timeout); 4xx errors are terminal |
When scraping metadata, bb executes the following stages:
- URL Normalization: Tracking parameters stripped (utm_*, fbclid, gclid, etc.), hosts lowercased, trailing slashes removed, protocol-relative URLs resolved
- Parallel Fetching: oEmbed, Plain HTML, Microlink, Peekalink, and Iframely fetchers run concurrently via thread pool
- oEmbed Support: Checks URL against provider registry (cached from oembed.com/providers.json with hardcoded fallback for top 15 providers). Supports YouTube, Vimeo, Twitter, Spotify, SoundCloud, TikTok, etc.
- Field Merging: Results merged by priority (oEmbed > HTML > Microlink > Peekalink > Iframely)
- Image Validation: Fetched images validated via magic byte detection (PNG/JPEG/WebP/GIF), decode check, minimum resolution >32x32. Rejects tracking pixels, HTML responses, corrupt data
- Headless Chrome Fallback: Launched only when no validated image is found. Includes stealth fingerprinting (deviceMemory, maxTouchPoints, WebGL vendor/renderer, AudioContext)
- Retry Logic: Failed tasks retried with exponential backoff (5s × 2^attempt + jitter) up to
task_queue_max_retriesfor transient errors (5xx, timeout, connection); 4xx errors are terminal
Create portable backups of your bb data:
# Create backup archive (default: ./bb-backup-{timestamp}.tar.gz)
bb backup
# Create backup at specific path
bb backup /path/to/backup.tar.gz
# Pipe backup to stdout (auto-detected when stdout is not a terminal)
bb backup > backup.tar.gz
docker compose run --rm bb bb backup > backup.tar.gz
# Restore from backup (prompts for confirmation)
bb import /path/to/backup.tar.gz
# Restore without confirmation
bb import /path/to/backup.tar.gz --yes
# Pipe backup into import (auto-detected when stdin is piped)
cat backup.tar.gz | bb import
docker compose run --rm -i bb bb import < backup.tar.gzIncluded in backups:
bookmarks.csv— All bookmark dataconfig.yaml— Configuration (user settings, read-only at runtime)rules.yaml— Automated rules (managed by the application)workspaces.yaml— Workspace definitionsuploads/— Preview images and favicons
Convert existing preview images to WebP format for reduced storage:
# Preview what would be compressed
bb compress --dry-run
# Compress images (prompts for confirmation)
bb compress
# Compress without confirmation
bb compress --yesgit clone https://github.com/bbonvi/bb.git
cd bb
cp .env.example .env
# Edit .env and set BB_AUTH_TOKEN (required)
# Production
docker compose up -d
# Development (cargo watch + vite dev server)
docker compose -f docker-compose.dev.yml upSee Running daemon in docker for details.
Requires Rust and optionally Node.js for the web UI.
git clone https://github.com/bbonvi/bb.git
cd bb
# Build backend
cargo build --release
sudo mv ./target/release/bb /usr/local/bin/bb
# Build web-ui (optional)
cd client
yarn install
yarn build # this will create bookmark and attempt to fetch metadata
bb add --url "https://github.com/bbonvi/bb"
# This will open up a neovim window where you can fill up the details
EDITOR=nvim bb add --editor
# output all bookmarks
bb search # start the daemon
RUST_LOG=info bb daemon
# this will connect to bb daemon at localhost:8080 and create a bookmark.
# --async-meta parameter makes it so daemon fetches metadata in background
# and you immediately get a response back, without a wait.
BB_ADDR=http://localhost:8080 bb add --async-meta --url "https://github.com/bbonvi/bb"
# after daemon completes the fetch, you can query bookmark by its title
BB_ADDR=http://localhost:8080 bb search --title bbProduction (docker-compose.yml):
docker compose up -d
docker compose logs -f # view logs
docker compose down # stopIncludes automatic restarts, health checks, memory limits (2GB), log rotation, and Cloudflare DNS.
Development (docker-compose.dev.yml):
docker compose -f docker-compose.dev.yml up
# Backend: http://localhost:8080
# Frontend: http://localhost:3000Mounts source directories — backend recompiles via cargo watch, frontend hot-reloads via Vite.
Standalone docker (without compose):
docker build -t bb:latest -f daemon.Dockerfile .
docker run --rm -it -v bb-data:/root/.local/share/bb -p 8080:8080 --name bb-daemon bb:latestWhen running bb as daemon, you can access webui at http://localhost:8080/
The extension saves pages and links directly to your BB server. Its toolbar popup lets you edit the URL, title, description, and tags. On a page already saved in BB, the popup loads its saved details and updates that bookmark.
Build from the repository root:
cd client
yarn install --frozen-lockfile
yarn build:extension- Open
chrome://extensions, enable Developer mode, and choose Load unpacked. - Select
client/dist-extensionin this repository and pin BB to the toolbar. - Open the BB icon, enter your BB server address and optional
BB_AUTH_TOKEN, then click Connect. Allow Chrome access to that server when prompted. Settings and the token stay in local extension storage.
Right-click a link to choose Send link to BB or Send current page to BB under the BB submenu. Elsewhere on a web page, Send current page to BB saves the page directly. A brief icon badge and a toast at the page's top right show the result. Errors preserve the URL for retry. Saving an existing URL keeps its details and reports Already in BB. Metadata is fetched by BB in the background.
Press Cmd+Shift+Y on macOS or Ctrl+Shift+Y on Windows/Linux to save the current page directly. Change the binding at chrome://extensions/shortcuts; if another extension already uses it, assign an available shortcut there.
After editing the extension, rebuild and click Reload on its card at chrome://extensions.
bb supports optional bearer token authentication for API routes. When enabled, both the web UI and CLI client require a valid token to access the API.
Enable authentication:
# Set a secure token (16+ characters recommended)
BB_AUTH_TOKEN=your-secret-token-here bb daemonCLI client with auth:
BB_ADDR=http://localhost:8080 BB_AUTH_TOKEN=your-secret-token-here bb searchBehavior:
- When
BB_AUTH_TOKENis unset or empty, authentication is disabled (backwards compatible) - When set, all
/api/*requests requireAuthorization: Bearer <token>header - Static files (web UI assets) are always accessible without authentication
- The web UI will prompt for the token on first access when auth is enabled
| Variable | Description | Default | Example |
|---|---|---|---|
RUST_LOG |
log level | error | warn |
BB_BASE_PATH |
Base path for bookmarks, configs and thumbnails | ~/.local/share/bb |
~/.local/share/bb |
BB_ADDR |
Daemon http address | http://localhost:8080 |
|
BB_AUTH_TOKEN |
Bearer token for API authentication. When set, all API requests require Authorization: Bearer <token> header. |
my-secret-token-1234 |
|
BB_BASIC_AUTH |
Optional basic auth for daemon authorization (deprecated, use BB_AUTH_TOKEN). |
myusername:mypassword |
|
HTTP_PROXY |
Proxy for all meta requests | socks5://127.0.0.1:8060 |
|
OPT_PROXY |
An optional proxy that will be used in case default (no proxy/HTTP_PROXY) request fails. Useful if bb needs to access region locked website, but you don't want to increase the probability of captcha. | socks5://127.0.0.1:8060 |
|
CHROME_PATH |
A path to chromium binary | chromium |
/usr/sbin/chromium |
EDITOR |
Your default text editor | vim |
nvim |
SHELL |
Shell to launch editor with | /usr/sbin/bash |
/bin/bash |
IFRAMELY_API_KEY |
Iframely API key for rich metadata extraction | abc123... |





