Introduction

LinkMarks is a local-first bookmark manager with deterministic deduplication, multi-device sync over CRDT, and an interactive TUI browser. It imports what you already have (Chromium, Firefox, Netscape HTML exports), deduplicates by canonical URL, keeps everything in a single SQLite store under ~/.local/share/linkmarks/, and optionally syncs across your devices through a self-hosted yrs-based relay.

TL;DR

# Install
cargo install linkmarks-cli --path crates/linkmarks-cli --bin linkmarks --locked

# Import from a browser export
linkmarks init
linkmarks import chromium ~/.cache/chromium/Default/Bookmarks

# Browse interactively
linkmarks tui

# Or script it
linkmarks list --format json | jq '.[] | select(.tags | index("rust"))'

What it is

LinkMarks treats bookmarks as first-class Rust data with five properties that matter:

  1. Local-first. The SQLite store is the source of truth. Sync is additive, never required for correctness. A user who never runs linkmarks sync has the same data as a user with three devices.
  2. Deterministic. Dedup is by canonical URL (sorted query params, lowercased host, no fragment, no trailing slash variance). Re-running linkmarks dedupe against the same input produces byte-identical output.
  3. Auditable. The codebase is 8 crates in a Cargo workspace. The core (linkmarks-core) is a library with no I/O outside the SQLite handle the caller hands it. The CLI (linkmarks-cli) is a thin clap wrapper. There is no telemetry, no analytics, no automatic network call.
  4. Multi-format import. Chromium's Bookmarks JSON, Firefox's places.sqlite, and Netscape-style HTML exports are all parsed natively. Round-trip fidelity is verified by unit tests for each bridge.
  5. Multi-device via yrs CRDT. When the operator chooses to run the self-hosted relay (preview), changes propagate as per-collection yrs sub-documents. Conflicts merge without operator action; the relay sees opaque bytes, not plaintext bookmarks.

What it is not

  • A bookmarking service. There is no central account. There is no SaaS. There is no "free tier" that costs you your privacy.
  • A web clipper. LinkMarks is for URLs, titles, tags, and notes. Not for full-page archives, screenshots, or rich text.
  • A read-it-later service. The TUI is a browser, not a reader. It does not download page contents.

Who it's for

LinkMarks is for operators who:

  • Care about the difference between m.example.com and example.com (canonical URL dedup handles this)
  • Run multiple devices and want bookmarks synced without trusting a third-party SaaS with the data
  • Like a TUI as their primary surface (ratatui + nucleo fuzzy search)
  • Want to be able to read every line of code that touches their bookmarks (the core is ~3,500 LOC, the whole workspace is ~12,000 LOC excluding linkmarks-bench-crdt)

Crate map

The workspace is 8 crates; the umbrella linkmarks is the canonical binary:

CrateVisibilityDescription
linkmarkspublic (umbrella)Single-binary that re-exports the CLI
linkmarks-clipublicThe clap-based command dispatch
linkmarks-corepublicLibrary: SQLite schema, dedup, sort, filter
linkmarks-tuipublicInteractive ratatui browser
linkmarks-bridge-chromiumpublicChromium Bookmarks JSON parser
linkmarks-bridge-firefoxpublicFirefox places.sqlite parser
linkmarks-bridge-netscapepublicNetscape HTML parser/serializer
linkmarks-bench-crdtprivateBenchmark harness (not published)

Every public crate ships to crates.io with the same Cargo.lock pinned via --locked install. The umbrella binary is documented at docs.rs/linkmarks; the library API is documented at the per-crate docs.rs page.

At a glance

PropertyValue
LanguageRust (edition 2021, MSRV 1.78)
LicenseAGPL-3.0-or-later OR LicenseRef-Commercial
StorageSQLite (single file under XDG)
Syncyrs CRDT, optional self-hosted relay (preview)
BridgesChromium JSON, Firefox places.sqlite, Netscape HTML
CLIlinkmarks (single binary)
TUIratatui + crossterm + nucleo
Runtime deps16 per crate (workspace-shared Cargo.lock)
Test count286 (workspace, release mode)
Headline LOC~12,000 across 8 crates (excluding bench)

Where to next

  • Getting startedcargo install and the first linkmarks import.
  • Concepts — the canonical URL model, the SQLite schema, the dedup algorithm.
  • CLI reference — every subcommand with flags.
  • TUI browser — keys, sort modes, filter modes.
  • Bridge formats — what's preserved per import.
  • Sync model — how the relay sees your bookmarks.
  • Architecture — the 8-crate workspace breakdown.
  • Hardening — operational hardening the reference deployment applies.
  • Reference — env vars, exit codes, file layout.
  • License — AGPL-3.0-or-later OR LicenseRef-Commercial.

Getting started

This page covers installing the linkmarks binary, performing the first-time store initialisation, and importing your first batch of bookmarks.

Install via cargo

cargo install linkmarks-cli \
    --git https://github.com/LOUST-PRO/LinkMarks \
    --tag v2.2.0 \
    --path crates/linkmarks-cli \
    --bin linkmarks \
    --locked

This places the binary at ~/.cargo/bin/linkmarks. The --locked flag pins the install to the lockfile shipped in the published crate, ensuring the same dependency graph that CI tests. The --path flag tells cargo which workspace member to build (the repo is a Cargo workspace with 8 crates).

Install from a package manager

Arch Linux

pacman -U linkmarks-2.2.0-1-x86_64.pkg.tar.zst

The PKGBUILD lives at arch/PKGBUILD in the repo and ships a linkmarks.install script with pre_upgrade and post_install hooks for migrating the SQLite store on version bumps.

Debian / Ubuntu

sudo dpkg -i linkmarks_2.2.0-1_amd64.deb

The Debian rules at debian/rules use dh $@ --buildsystem cargo with a dh-cargo integration, and respect DEB_BUILD_OPTIONS=nocheck in the test override.

Fedora / RHEL

sudo dnf install ./linkmarks-2.2.0-1.fc42.x86_64.rpm

The spec at rpm/linkmarks.spec uses rpmlint-clean license metadata (License: AGPL-3.0-or-later).

Homebrew

brew install LOUST-PRO/tap/linkmarks

The Formula at homebrew/Formula/linkmarks.rb uses brew audit --strict-clean metadata.

First-time init

The first run creates the XDG store directory and a default config:

linkmarks init
# expected output: "Initialised linkmarks store at ~/.local/share/linkmarks/linkmarks.db"
#                  "Wrote default config to ~/.config/linkmarks/config.toml"

The default config (config.toml.example shows every field with comments) covers:

  • The SQLite path (default: XDG-resolved)
  • The default sort mode (one of updated, title, canonical-url, created)
  • The default filter mode (1 of 3 — see Concepts)
  • The sync relay URL (only used when linkmarks sync runs)

linkmarks init is idempotent — re-running it is safe.

Importing your first bookmarks

From Chromium (Brave, Edge, Opera, etc.)

Chromium-family browsers store bookmarks at <profile>/Bookmarks as a JSON file. Find your profile with chrome://version (the "Profile Path" line).

linkmarks import chromium ~/.config/chromium/Default/Bookmarks
# expected output: "Imported 1247 bookmarks from chromium"
#                  "  - 1189 kept after canonical-URL dedupe"
#                  "  - 58 duplicates suppressed"

The importer:

  1. Parses Chromium's bookmark_bar, other, and synced folders.
  2. Resolves Chromium's internal node IDs to deterministic ULIDs.
  3. Canonicalises every URL (sorted query params, lowercased host, no fragment).
  4. Deduplicates by canonical URL — last write wins per URL.
  5. Imports folder structure as tag-prefix tags (e.g. bar/Tech/Rust becomes ["bar", "Tech", "Rust"]).

From Firefox

Firefox stores bookmarks at <profile>/places.sqlite. Find your profile with about:profiles.

linkmarks import firefox ~/.mozilla/firefox/abc123.default-release/places.sqlite

The Firefox bridge reads moz_bookmarks (folder structure) and moz_places (URL/title/timestamp). It does NOT import moz_annos (annotations) by default; pass --with-annotations to include them.

From Netscape HTML (Pocket, Raindrop, Pinboard exports)

linkmarks import netscape ~/Downloads/pocket-export.html

The Netscape bridge parses the standard <DL> / <A HREF> hierarchy. Tags from <DD> comments are preserved.

Verify the install

Two sanity checks confirm the store is alive:

linkmarks --version
# expected: linkmarks 2.2.0

linkmarks list --limit 5
# expected: table with the 5 most-recently-updated bookmarks

If linkmarks list returns Error::StoreNotInitialised, run linkmarks init first.

Uninstalling

cargo uninstall linkmarks-cli
rm -rf ~/.local/share/linkmarks
rm -rf ~/.config/linkmarks

The SQLite store is the only on-disk state. Removal is fully reversible.

Concepts

This page explains the four concepts the rest of LinkMarks builds on: canonical URL, the SQLite schema, the dedup algorithm, and the sort/filter modes. Understanding these makes the CLI reference and the TUI browser feel like obvious consequences.

Canonical URL

The single most important concept in LinkMarks is the canonical URL: a normalised form of a URL where two URLs that "look the same" to a human are byte-identical after canonicalisation.

A canonical URL is computed by applying these rules, in order:

  1. Lowercase the scheme (https:// stays https://).
  2. Lowercase the host (Example.COM becomes example.com).
  3. Remove the default port for the scheme (:443 for https, :80 for http).
  4. Remove the fragment (#section-2 is gone).
  5. Remove trailing slash on the path (except for the empty path itself, which stays /).
  6. Sort query parameters lexicographically by name, then by value.
  7. Recode percent-encoding to NFC unicode normalisation.

After canonicalisation, the following URLs are all byte-identical:

https://Example.com/foo?b=2&a=1
https://example.com/foo?a=1&b=2
HTTPS://example.com:443/foo/?b=2&a=1#section-2

This matters because two bookmark managers will routinely create the "same" bookmark under different strings. Without canonical URLs, dedup is a heuristic; with canonical URLs, dedup is a lookup.

SQLite schema

The store is a single SQLite file. The schema (v2.2.0) has 7 tables:

TablePurpose
bookmarksThe bookmark records themselves
tagsTag definitions (id + canonical name)
bookmark_tagsMany-to-many bookmark ↔ tag
foldersFolder hierarchy (for Chromium-style import)
bookmark_foldersMany-to-many bookmark ↔ folder
sync_logSync metadata (last seen, last pushed)
schema_versionSingle-row schema version table

bookmarks columns

id            ULID primary key
canonical_url TEXT NOT NULL  -- the canonical form (unique index)
original_url  TEXT NOT NULL  -- the first-seen raw URL
title         TEXT NOT NULL
notes         TEXT NULL
created_at    TEXT NOT NULL  -- RFC3339 timestamp
updated_at    TEXT NOT NULL
last_visit_at TEXT NULL      -- null if never visited
visit_count   INTEGER NOT NULL DEFAULT 0

The unique index on canonical_url enforces dedup at the storage layer. An INSERT of a duplicate canonical URL fails with SQLITE_CONSTRAINT_UNIQUE, which the importer treats as "already-imported, skip".

The dedup algorithm

linkmarks dedupe walks the entire bookmarks table, groups by canonical_url, and for each group keeps one record per the following tie-breakers (in order):

  1. Most recent updated_at — the most-recently-touched record wins.
  2. Most recent last_visit_at — among records with the same updated_at, the most-recently-visited wins.
  3. Highest visit_count — among ties on timestamps, the most-visited wins.
  4. Lexicographically smallest id — final deterministic fallback.

The losing records are deleted; their bookmark_tags and bookmark_folders rows are re-parented to the winner before the delete. The whole operation is a single transaction with PRAGMA foreign_keys = ON (cascading deletes).

Re-running linkmarks dedupe against the same store produces byte-identical output. This is verified by the property tests in linkmarks-core/src/dedupe.rs.

Sort modes

The TUI browser and the linkmarks list command both support four sort modes:

ModeComparator
updatedupdated_at DESC (default)
titletitle COLLATE NOCASE ASC
canonical-urlcanonical_url ASC
createdcreated_at DESC

The sort modes are enumerated as SortMode in linkmarks-core. The TUI cycles through them with the s key; the CLI accepts --sort as a flag.

Filter modes

The TUI's filter input (/ key) supports three filter modes:

ModeDescription
SubstringDefault. The query is matched as a substring against title + URL + tags. Case-insensitive.
TagThe query is matched as a tag prefix (e.g. rus matches rust, rust-cli, rustr but not cru).
FuzzyThe query is matched using nucleo's fuzzy matcher. rs tmpl matches ratatui-template.

The filter modes are enumerated as FilterMode in linkmarks-tui. The TUI cycles through them with Ctrl+F.

Sync model (preview)

The self-hosted relay is preview-stage in v2.2.0. When enabled:

  • Each device writes to its local SQLite store.
  • linkmarks sync push serialises the changed rows into a per-collection yrs sub-document.
  • The relay receives opaque yrs bytes + an HTTP path; it does not see plaintext bookmarks.
  • Each devices pull merges the remote yrs bytes into its local store, applying the canonical-URL dedup on receive.

The relay itself is preview; the linkmarks sync --remote CLI is fully wired and tested but the linkmarks-relay binary is in a future release. The architecture is documented in Architecture.

CLI reference

The linkmarks binary exposes 13 subcommands. Every subcommand supports --help, accepts -h as shorthand, and exits 0 on success, 1 on user error, 2 on store error, 3 on import/parse error.

Global flags

linkmarks [OPTIONS] <COMMAND>

Options:
  -C, --config <PATH>    Override config file path (default: XDG)
  -v, --verbose          Increase log verbosity (-v, -vv, -vvv)
  -q, --quiet            Suppress non-error output
  -V, --version          Print version and exit
  -h, --help             Print help and exit

Subcommands

init

Initialise the store and config file.

Usage: linkmarks init [OPTIONS]

Options:
      --force            Reinitialise even if store exists
      --store-dir <PATH> Override the XDG store directory

import

Import bookmarks from a browser export.

Usage: linkmarks import <FORMAT> <SOURCE>

Arguments:
  <FORMAT>   One of: chromium, firefox, netscape
  <SOURCE>   Path to the export (file or directory depending on format)

Options:
      --dry-run          Show what would be imported without writing
      --limit <N>        Cap the number of records imported
      --with-annotations  Firefox only: include moz_annos

Examples:

linkmarks import chromium ~/.config/chromium/Default/Bookmarks
linkmarks import firefox ~/.mozilla/firefox/abc.default/places.sqlite --with-annotations
linkmarks import netscape ~/Downloads/pocket-export.html --dry-run

list

List bookmarks.

Usage: linkmarks list [OPTIONS]

Options:
      --limit <N>          Cap the number of records shown
      --sort <MODE>        One of: updated, title, canonical-url, created
      --filter <MODE>      One of: substring, tag, fuzzy
      --query <Q>          Pre-populate the filter query
      --format <FMT>       One of: table, json, csv, plain
      --tag <TAG>          Filter to records carrying <TAG> (repeatable)
      --folder <PATH>      Filter to records in <PATH> (repeatable)
      --no-header          Omit header row (table, csv formats)

Examples:

linkmarks list --limit 10
linkmarks list --tag rust --format json
linkmarks list --query ratatui --filter fuzzy
linkmarks list --folder "Tech/Rust" --folder "Tech/Go"

add

Add a single bookmark.

Usage: linkmarks add <URL> [OPTIONS]

Arguments:
  <URL>     The URL to bookmark

Options:
  -t, --title <TITLE>    Bookmark title (defaults to <URL>)
  -T, --tag <TAG>        Tag (repeatable)
  -n, --notes <NOTES>    Free-form notes
      --folder <PATH>    Place in folder hierarchy

delete

Delete a bookmark by ID or canonical URL.

Usage: linkmarks delete <TARGET>

Arguments:
  <TARGET>    ULID or canonical URL

Options:
      --dry-run    Show what would be deleted
      --force      Skip confirmation prompt

update

Update bookmark fields.

Usage: linkmarks update <TARGET> [OPTIONS]

Arguments:
  <TARGET>    ULID or canonical URL

Options:
      --title <TITLE>    Set title
      --notes <NOTES>    Set notes
      --add-tag <TAG>    Add tag (repeatable)
      --rm-tag <TAG>     Remove tag (repeatable)
      --folder <PATH>    Set folder

dedupe

Run the canonical-URL dedupe pass.

Usage: linkmarks dedupe [OPTIONS]

Options:
      --dry-run         Show the dedupe plan without executing
      --limit <N>       Cap the number of groups processed
      --verbose         Print per-group winners and losers

export

Export the store to another format.

Usage: linkmarks export <FORMAT> [OPTIONS]

Arguments:
  <FORMAT>    One of: netscape, json, csv

Options:
  -o, --output <PATH>    Output path (default: stdout)
      --limit <N>        Cap records exported
      --query <Q>        Filter query

sync

Push or pull from the self-hosted relay (preview).

Usage: linkmarks sync <DIRECTION> [OPTIONS]

Arguments:
  <DIRECTION>    One of: push, pull, status

Options:
      --remote <URL>     Override the relay URL from config
      --dry-run          Show what would be sent/received
      --limit <N>        Cap the number of records
      --since <RFC3339>  Only push/pull records updated since

tui

Launch the interactive TUI browser.

Usage: linkmarks tui [OPTIONS]

Options:
      --sort <MODE>      Initial sort mode
      --filter <MODE>    Initial filter mode
      --query <Q>        Pre-populate filter query
      --theme <NAME>     Color theme: rust, light, dark, ayu

completions

Generate shell completions.

Usage: linkmarks completions <SHELL>

Arguments:
  <SHELL>    One of: bash, zsh, fish, elvish, powershell

Options:
  -o, --output <PATH>    Output path (default: stdout)

doctor

Diagnose the install.

Usage: linkmarks doctor

Options:
      --check <NAME>     Run a single check (one of: store, config, indices, sync)

Checks:
  store      Verifies the SQLite file exists and is readable
  config     Validates config.toml against the schema
  indices    Confirms unique indexes on canonical_url exist
  sync       Tests connectivity to the configured relay

version

Print version, build profile, and feature flags.

Usage: linkmarks version

Output:
  linkmarks 2.2.0
  profile: release
  features: default
  sqlite: 3.46.1
  yrs: 0.18.5
  cargo: 1.84.0
  rustc: 1.84.0 (edition 2021)

Exit codes

CodeMeaning
0Success
1User error (bad flags, missing arguments)
2Store error (SQLite open, schema mismatch)
3Import/parse error (malformed input)
4Sync error (relay unreachable, conflict)
5Permission error (cannot read source, cannot write store)
64Configuration error (config.toml invalid)

The exit codes are stable across releases; downstream tooling can rely on them.

TUI browser

The TUI browser (linkmarks tui) is the primary interactive surface. It runs in any terminal, uses ratatui for rendering and crossterm for input, and indexes the store into an in-memory nucleo matcher for sub-millisecond filter feedback.

Layout

┌──────────────────────────────────────────────────────────────────────┐
│ LinkMarks ~/local/share/linkmarks/linkmarks.db                       │
├──────────────────────────────────────────────────────────────────────┤
│ / rust tmpl ▏                                                          │
├──────────────────────────────────────────────────────────────────────┤
│ ▎ Updated 2026-08-12  rust-template  https://github.com/.../tmpl      │
│ ▎ Updated 2026-08-10  rust-analyzer  https://rust-analyzer.github.io │
│   Updated 2026-08-05  rust-2024     https://doc.rust-lang.org/...    │
│ ...                                                                    │
├──────────────────────────────────────────────────────────────────────┤
│ 1/1247 updated:rust,cli  s:sort  Ctrl+F:filter  ?:help  q:quit        │
└──────────────────────────────────────────────────────────────────────┘

The TUI has 5 regions: header (store path), filter bar, list, status bar. The list selection cursor () can be moved with j/k or arrow keys.

Keymap

KeyAction
j / Move cursor down
k / Move cursor up
gJump to first record
GJump to last record
/Enter filter mode
EscClear filter, exit help/menu
EnterOpen the highlighted URL in the default browser
TabToggle tag picker
sCycle sort mode
Ctrl+FCycle filter mode
aAdd a bookmark (prompts for URL + title)
eEdit the highlighted bookmark
dDelete the highlighted bookmark (with confirmation)
rRefresh from disk
?Toggle help overlay
q / Ctrl+CQuit

Filter modes

Pressing Ctrl+F cycles through three modes; the active mode is shown in the status bar.

Substring (default)

The query is matched as a substring against title, URL, and tags. Case-insensitive. Use this for direct keyword search.

Examples:

  • rust matches every bookmark whose title, URL, or tags contain "rust" (case-insensitive)
  • cli template matches records containing both substrings (in any field)

Tag

The query is matched as a tag prefix. Useful for narrowing by categorisation without typing the full tag.

Examples:

  • ru matches tags rust, rust-cli, rustr but NOT cru
  • tech/ matches tags tech/rust, tech/go (forward-slash is literal)

Fuzzy

The query is matched using nucleo's fuzzy matcher. Allows out-of-order tokens and small typos.

Examples:

  • rs tmpl matches rust-template, ratatui-template, crate-template-rs
  • linkmark matches linkmarks, link-marker, l-i-n-k-marks

Sort modes

Pressing s cycles through four modes; the active mode is shown in the status bar.

ModeComparator
updatedupdated_at DESC (default)
titletitle COLLATE NOCASE ASC
canonical-urlcanonical_url ASC
createdcreated_at DESC

The sort persists across filter changes within the same session.

Actions

Open URL

Enter opens the highlighted URL via xdg-open (Linux), open (macOS), or start (Windows). If the URL is malformed, the TUI shows a notification and continues.

Add a bookmark

a opens an input prompt for URL; pressing Enter opens a second prompt for title (with the URL as default). Tags are added by Tab-completion against existing tags.

Edit a bookmark

e opens an edit form for the highlighted record: title, notes, tags. Save with Enter; cancel with Esc.

Delete a bookmark

d opens a confirmation prompt. Default is No (cursor on No). Press Tab to switch to Yes, then Enter to delete. The delete is reversible: u (undo) restores the last 10 deletes within the session.

Themes

The TUI supports 4 themes, switchable with :theme <name> from the help overlay:

  • rust (default): the rust-lang.org palette
  • light: high-contrast light theme
  • dark: low-contrast dark theme
  • ayu: Ayu-inspired theme

Configuration

The TUI reads its keymap and theme from the global config. To customise the keymap, write a ~/.config/linkmarks/keymap.toml:

[keymap]
quit = ["q", "Ctrl+C", "Esc"]
open = ["Enter", "o"]
delete = ["d", "Backspace"]

The default keymap is documented in linkmarks-tui/src/input.rs.

Bridge formats

LinkMarks imports bookmarks from 3 source formats and exports to 2. Each bridge is a separate crate (linkmarks-bridge-*) with its own dependency tree, its own test fixture, and its own round-trip fidelity test.

Chromium / Brave / Edge / Opera

The Chromium bridge (linkmarks-bridge-chromium) parses the Bookmarks JSON file produced by Chromium-family browsers.

File location

Linux:    ~/.config/<browser>/Default/Bookmarks
macOS:    ~/Library/Application Support/<browser>/Default/Bookmarks
Windows:  %LOCALAPPDATA%\<browser>\User Data\Default\Bookmarks

The file is written by the browser on every bookmark change; the importer reads it once and disconnects.

Schema mapping

Chromium fieldLinkMarks field
bookmark_bar.children[]records + folder bar/
other.children[]records + folder other/
synced.children[]records + folder synced/
children[].urloriginal_url (canonicalised on import)
children[].nametitle
children[].date_addedcreated_at
children[].date_last_usedlast_visit_at (when present)
children[].iddiscarded (LinkMarks assigns its own ULID)
children[].guiddiscarded
children[].meta_infonotes (when present)
Folder pathsbookmark_folders rows

What's preserved

  • URL (canonicalised)
  • Title
  • Date added → created_at
  • Date last used → last_visit_at (when non-zero)
  • Folder hierarchy (the importer splits /-separated folder paths into ancestor folders)
  • Tags from meta_info (when present)

What's NOT preserved

  • Favicon (not stored in the Bookmarks JSON)
  • Visit count (Chromium does not embed this; the importer starts from 0 and the user's browsing history is not accessible)
  • Sync metadata (the synced folder is preserved as a folder but the per-device attribution is not)
  • Internal IDs (LinkMarks assigns its own ULIDs)

Round-trip test

The Chromium bridge ships with a fixture (fixtures/chromium-v123.bookmarks) and a round-trip test:

  1. Import fixture → store A
  2. Export store A as Netscape HTML
  3. Import the Netscape HTML → store B
  4. Compare A and B by canonical URL

The test passes when every record in A has a matching canonical URL in B with the same title, tags, and folder path.

Firefox

The Firefox bridge (linkmarks-bridge-firefox) reads a places.sqlite SQLite file directly.

File location

Linux:    ~/.mozilla/firefox/<profile>/places.sqlite
macOS:    ~/Library/Application Support/Firefox/Profiles/<profile>/places.sqlite
Windows:  %APPDATA%\Mozilla\Firefox\Profiles\<profile>\places.sqlite

The bridge reads the file read-only; the importer does not require Firefox to be closed.

Schema mapping

Firefox table.columnLinkMarks field
moz_places.urloriginal_url
moz_places.titletitle
moz_places.last_visit_datelast_visit_at
moz_bookmarks.dateAddedcreated_at
moz_bookmarks.lastModifiedupdated_at
moz_bookmarks.titlefolder title (when type = 2)
moz_bookmarks.type = 1bookmark record
moz_bookmarks.type = 2folder record
moz_bookmarks.type = 3separator (discarded)
moz_annos.content (when --with-annotations)notes

What's preserved

  • URL (canonicalised)
  • Title
  • Folder hierarchy
  • Tags from moz_tags (when present; Firefox's tagging was introduced in Firefox 96)
  • Annotations (when --with-annotations)
  • Date added / date modified / date last visited

What's NOT preserved

  • Favicons (stored in favicons.sqlite, not in places.sqlite)
  • Tags from places.sqlite without the --with-annotations flag (the import is much faster without annotations; the SQLite ATTACH is the expensive part)
  • History (only bookmarks are imported; the importer filters by moz_bookmarks.type IN (1, 2))
  • Visit counts (not stored in places.sqlite; Firefox tracks visits separately in moz_historyvisits)

Live Firefox warning

If Firefox is running while linkmarks import firefox executes, the bridge reads the file in read-only mode. SQLite WAL mode is honored; the importer does not block Firefox's writes.

Netscape HTML

The Netscape bridge (linkmarks-bridge-netscape) parses the canonical <DL><DT><A HREF> HTML format used by every browser export ever and by Pocket / Raindrop / Pinboard exports.

Example input

<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
    <DT><H3 ADD_DATE="1723500000">Tech</H3>
    <DL><p>
        <DT><A HREF="https://example.com/" ADD_DATE="1723500001">Example</A>
        <DD>example-tag
        <DT><A HREF="https://github.com/" ADD_DATE="1723500002">GitHub</A>
    </DL><p>
</DL><p>

Schema mapping

Netscape attributeLinkMarks field
HREForiginal_url
Text content of <A>title
ADD_DATE (epoch seconds)created_at
LAST_MODIFIED (epoch seconds)updated_at (when present)
TAGS (comma-separated, post-2008)tags
Text content of <DD> (Pocket style)tags (split by ,)
<H3> headingsfolder titles
Nested <DL>folder hierarchy

What's preserved

  • URL (canonicalised)
  • Title
  • Tags (from TAGS attribute or <DD> comment)
  • Folder hierarchy (from <H3> headings)
  • Date added / date modified

What's NOT preserved

  • Favicons (not in the Netscape format)
  • Visit history (not in the Netscape format)
  • Per-record GUIDs (LinkMarks assigns its own ULIDs)

Round-trip test

The Netscape bridge's round-trip test is identical to Chromium's: import → export → re-import → compare by canonical URL.

Export formats

linkmarks export <FORMAT> writes the store in the chosen format. Two formats are supported:

  • netscape — produces a <DL> HTML file, ready for import into another browser or service
  • json — produces a per-record JSON array, suitable for piping into jq
  • csv — produces a CSV with columns matching the linkmarks list --format csv schema

Choosing a bridge

If you have multiple source formats, import each one in order of size:

  1. Firefox (most complete: tags + annotations)
  2. Chromium (most universally available)
  3. Netscape HTML (least information, but the most portable)

The canonical-URL dedupe is idempotent, so importing multiple sources is safe.

Sync model

This page documents the multi-device sync model used by LinkMarks v2.2.0 and later. The sync layer is preview in v2.2.0 — the CLI is fully wired and tested, but the relay binary is in a future release. The data model and conflict-resolution rules are stable.

Design goals

  1. Local-first. A device that has never run linkmarks sync is functionally identical to one that syncs daily. Sync is additive, never required.
  2. Operator-controlled. The relay is self-hosted; the operator controls the server, the storage, the retention, and the access logs.
  3. Privacy by design. The relay sees opaque yrs bytes, not plaintext bookmarks. The relay cannot decrypt without the device-shared key, which is configured out-of-band.
  4. Conflict-free. Two devices editing the same bookmark concurrently converge to the same final state without operator intervention.
  5. Auditable. The sync layer is ~600 LOC and lives in linkmarks-core/src/sync/; every merge operation has unit tests with deterministic fixtures.

Architecture

   ┌──────────┐                  ┌──────────┐                  ┌──────────┐
   │ Device A │                  │  Relay   │                  │ Device B │
   │  SQLite  │                  │  (yrs    │                  │  SQLite  │
   │  + yrs   │  ──yrs bytes──▶  │  opaque  │ ──yrs bytes──▶  │  + yrs   │
   │  snapshot│                  │  storage)│                  │  snapshot│
   └──────────┘                  └──────────┘                  └──────────┘

Each device has a local SQLite store. Sync serialises the changed rows into a per-collection yrs sub-document and pushes the opaque bytes to the relay. The relay stores the bytes keyed by a per-collection name (bookmarks, tags, folders) and a per-device device_id.

On pull, the device fetches the latest remote bytes for each collection and merges them into its local yrs sub-document. The SQLite rows are then re-derivable from the merged yrs snapshot.

The relay

The relay (linkmarks-relay, future release) is a tiny HTTP server with 4 endpoints:

POST /v1/push/{collection}
     Headers: Authorization: Bearer <device-token>
     Body: opaque yrs bytes
     Response: 204 No Content

GET  /v1/pull/{collection}
     Headers: Authorization: Bearer <device-token>
     Response: 200 OK with body = opaque yrs bytes

GET  /v1/state/{collection}
     Headers: Authorization: Bearer <device-token>
     Response: { "version": <u64>, "device_count": <u32> }

GET  /healthz
     Response: 200 OK with body = { "version": "<relay-version>" }

The relay stores bytes in a per-collection file under /var/lib/linkmarks-relay/. There is no index, no query layer, no plaintext ever.

Conflict resolution

Conflicts arise when two devices edit the same bookmark concurrently. The yrs CRDT resolves them by Lamport timestamp order. Specifically:

  1. Each device tracks a monotonically-increasing lamport_clock counter.
  2. Every mutation is stamped with (device_id, lamport_clock).
  3. On merge, the mutation with the higher lamport_clock wins.
  4. Ties are broken by device_id lexicographic order.

Bookmark fields are merged per-field (not per-record), so two devices editing different fields of the same bookmark never overwrite each other.

Example:

Device A (clock=42): edit https://example.com -> title="Example"
Device B (clock=41): edit https://example.com -> tags=["rust"]

After merge, the bookmark has title="Example" (from A, clock 42) and tags=["rust"] (from B, clock 41). The merge is automatic; neither edit is lost.

What syncs

The sync layer covers 4 entity types:

EntityPer-collection nameConflict policy
bookmarksbookmarksfield-level merge
tagstagsset union
foldersfolderslast-writer-wins (tree shape is single-writer in practice)
sync_lognot syncedper-device, never shared

The bookmark_tags and bookmark_folders join tables are derived locally from bookmarks + tags + folders after each pull.

What does NOT sync

  • The local config.toml — each device has its own configuration.
  • The local keymap.toml — same.
  • The last_visit_at and visit_count fields — these are per-device browsing activity, not shared bookmarks.
  • The original URL — only the canonical URL is synced. The first device to see a URL wins for original_url.

Sync CLI

# Push local changes to the relay
linkmarks sync push

# Pull remote changes into local
linkmarks sync pull

# Show sync state
linkmarks sync status

# Push with custom relay URL
linkmarks sync push --remote https://relay.example.com

linkmarks sync is idempotent — pushing with no local changes is a no-op; pulling with no remote changes is a no-op.

Sync timing

By default, linkmarks sync runs when invoked. There is no built-in cron. Operators who want background sync typically wrap it in a systemd timer:

# ~/.config/systemd/user/linkmarks-sync.service
[Unit]
Description=LinkMarks background sync
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/linkmarks sync push
ExecStart=/usr/bin/linkmarks sync pull

# ~/.config/systemd/user/linkmarks-sync.timer
[Unit]
Description=Run LinkMarks sync every 15 minutes

[Timer]
OnCalendar=*:0/15
Persistent=true

[Install]
WantedBy=timers.target

Threat model

The relay's threat model is documented in the Hardening page. The short version: the relay is untrusted from the perspective of the bookmark contents; it stores opaque bytes and cannot decrypt. The relay is trusted from the perspective of availability and storage integrity.

Architecture

The LinkMarks workspace is 8 Cargo crates organised in 4 layers: core library, bridges, TUI, CLI/umbrella. This page documents the dependency graph, the layer boundaries, and the where-to-find-what map for contributors.

Workspace layout

LinkMarks/
├── Cargo.toml                # Workspace root
├── Cargo.lock                # Shared lockfile
├── crates/
│   ├── linkmarks/           # Umbrella binary (re-exports CLI)
│   ├── linkmarks-cli/       # Command dispatch
│   ├── linkmarks-core/      # Library: SQLite, dedup, sort, filter, sync
│   ├── linkmarks-tui/       # Interactive ratatui browser
│   ├── linkmarks-bridge-chromium/
│   ├── linkmarks-bridge-firefox/
│   ├── linkmarks-bridge-netscape/
│   └── linkmarks-bench-crdt/  # Private benchmark harness
├── docs/
│   ├── man/linkmarks.1       # Canonical man page
│   └── relay-deployment.md   # Self-hosted relay guide (preview)
├── book/                    # This mdbook site
├── arch/PKGBUILD
├── debian/
├── rpm/
└── homebrew/Formula/

Dependency graph

                       ┌────────────────────┐
                       │     linkmarks      │  (umbrella binary)
                       └─────────┬──────────┘
                                 │
                                 ▼
                       ┌────────────────────┐
                       │   linkmarks-cli    │
                       └─────────┬──────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              │                  │                  │
              ▼                  ▼                  ▼
      ┌─────────────┐    ┌──────────────┐    ┌─────────────┐
      │ linkmarks-  │    │ linkmarks-   │    │ linkmarks-  │
      │   core      │    │   tui        │    │   bridges   │
      └─────────────┘    └──────┬───────┘    └──────┬──────┘
                                │                   │
                                ▼                   ▼
                       ┌─────────────┐    ┌──────────────────────┐
                       │ linkmarks-  │    │ linkmarks-bridge-*   │
                       │   core      │    │ (chromium/firefox/   │
                       │             │    │  netscape)           │
                       └─────────────┘    └──────────────────────┘

The bridges depend on linkmarks-core for the canonical-URL helper. The TUI depends on linkmarks-core for the SQLite reader. The CLI depends on all of them.

Layer responsibilities

linkmarks-core (~3,500 LOC)

The pure-library layer. Has zero network I/O, zero CLI integration, zero TUI integration. Public API surface:

ModulePurpose
canonicalCanonical URL computation
schemaSQLite schema + migrations
storeCRUD over the SQLite store
dedupeDeduplication pass
sortSort mode enum + comparators
filterFilter mode enum + matchers
bridgeBridge trait + shared types
syncyrs sub-document merge

linkmarks-bridge-* (~400 LOC each)

Format-specific parsers. Each bridge:

  1. Implements the Bridge trait from linkmarks-core.
  2. Has its own dependency tree (e.g. linkmarks-bridge-firefox uses rusqlite to read places.sqlite; the others don't).
  3. Has its own fixture under tests/fixtures/.
  4. Has a round-trip test that imports → exports → re-imports.

The three bridges share no code beyond the Bridge trait.

linkmarks-tui (~2,200 LOC)

The interactive ratatui browser. Depends on linkmarks-core for the read-only store, on nucleo for fuzzy matching, and on crossterm for terminal I/O.

Public API surface:

ModulePurpose
appThe App struct (state machine)
inputKey event → action dispatcher
renderratatui render loop
themeColor theme registry

The TUI does not depend on linkmarks-cli. It is a separate binary that ships alongside the CLI.

linkmarks-cli (~1,800 LOC)

The clap-based command dispatcher. Public API surface:

ModulePurpose
mainclap parser + dispatch
cmdPer-subcommand implementations
configConfig file loader
outputOutput formatting (table, json, csv)

The CLI is the only crate that wires linkmarks-core, linkmarks-tui, and the bridges together.

linkmarks (umbrella, ~50 LOC)

The single-binary umbrella that re-exports the CLI's main:

fn main() {
    linkmarks_cli::run();
}

The umbrella exists so users can cargo install linkmarks and get a single canonical binary, while library users can depend on linkmarks-core directly.

linkmarks-bench-crdt (private)

Benchmark harness for the yrs CRDT merge path. NOT published to crates.io. Not in the umbrella binary. The CI runs the benchmarks nightly and posts results to a private dashboard.

Public API contracts

Every public crate ships an API.toml (a small TOML manifest) documenting the public surface. The CI enforces that no pub symbol is added without an entry in API.toml.

This is the guarantee that lets LinkMarks ship semver-meaningful minor releases without breaking downstream library users.

Where to find what

If you want to...Look at...
Add a new sort modelinkmarks-core/src/sort.rs
Add a new filter modelinkmarks-core/src/filter.rs
Add a new bridgecopy linkmarks-bridge-netscape/, edit the parser
Add a new CLI subcommandlinkmarks-cli/src/cmd/
Add a new TUI keybindinglinkmarks-tui/src/input.rs
Add a new SQLite columnlinkmarks-core/src/schema.rs + write a migration
Change the canonical URL ruleslinkmarks-core/src/canonical.rs
Change the yrs merge semanticslinkmarks-core/src/sync/
Add a new export formatlinkmarks-cli/src/cmd/export.rs
Add a new themelinkmarks-tui/src/theme/

Build flags

FeaturePurpose
defaultStable features
syncyrs CRDT sync layer
benchBenchmark harness (private only)
static-sqliteBundled SQLite (no system dep)

The default feature set is ["sync"]. Disabling sync removes the linkmarks sync subcommand and the yrs dependency from the core library.

Hardening

This page documents the operational hardening applied to the reference LinkMarks deployment. It is the documentation of docs/relay-deployment.md extended with the threat model and the day-2 operations procedures.

Threat model

LinkMarks has three trust boundaries:

  1. Local device. The device is fully trusted. The operator owns the SQLite store; the TUI is a normal user-space application. No sandboxing is needed.

  2. Relay. The relay is untrusted from the perspective of bookmark contents (it sees opaque yrs bytes). It is trusted from the perspective of availability and storage integrity: the operator runs the relay on infrastructure they control.

  3. Network. The network is untrusted. All transport is HTTPS + bearer-token authentication. The relay TLS cert is operator-managed (Let's Encrypt via certbot).

What an attacker can do

AttackerCapabilityMitigation
Network observerSees TLS-encrypted HTTP trafficTLS 1.3 + HSTS
Network MITMCan break TLS only with operator's CAcert pinning (optional, off by default)
Relay operator (or relay attacker)Sees opaque yrs bytes, version vectorsCannot decrypt without device-shared key
Compromised deviceSees plaintext bookmarksNot in scope (device is fully trusted)
Compromised SQLite fileSees plaintext bookmarksUse FDE + dm-crypt on the device

What an attacker cannot do

  • Read plaintext bookmarks by compromising the relay alone.
  • Forge mutations from another device (bearer-token auth + yrs lamport ordering).
  • Roll back to a previous state without consent (yrs vectors are append-only; deletion requires explicit operator action).
  • Cause data loss across devices by compromising a single device (mutations are field-merged, not record-replaced).

Reference deployment

The reference deployment uses:

  • A single VPS running the relay behind a reverse proxy
  • A reverse proxy (nginx) terminating TLS + serving ACME
  • A separate storage volume for the relay's opaque bytes
  • systemd unit for the relay, enabled at boot

nginx configuration

server {
    listen 443 ssl;
    server_name relay.example.com;

    ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem;
    ssl_protocols TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;

    location / {
        proxy_pass http://127.0.0.1:8787;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /healthz {
        proxy_pass http://127.0.0.1:8787/healthz;
        access_log off;
    }
}

The relay listens on 127.0.0.1:8787 and is unreachable from the network directly.

systemd unit

[Unit]
Description=LinkMarks relay (preview)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=linkmarks-relay
Group=linkmarks-relay
WorkingDirectory=/var/lib/linkmarks-relay
Environment=LM_RELAY_LISTEN=127.0.0.1:8787
Environment=LM_RELAY_DATA_DIR=/var/lib/linkmarks-relay/collections
Environment=LM_RELAY_TOKEN=<32-byte hex token>
ExecStart=/usr/local/bin/linkmarks-relay
Restart=on-failure
RestartSec=5s

NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/linkmarks-relay
PrivateTmp=true
MemoryMax=64M
MemoryHigh=48M
CPUQuota=50%

[Install]
WantedBy=multi-user.target

The relay runs under a dedicated user, with ProtectSystem=strict read-only system access, and a 64 MiB RSS cap.

Backup

The relay's data directory (/var/lib/linkmarks-relay/) should be backed up nightly. The opaque bytes are:

  • Per-collection files (bookmarks, tags, folders)
  • A small metadata.json with version vectors

A 30-day retention is sufficient: the operator can always reconstruct the state from any device's local SQLite store by running linkmarks sync push, so the relay is a coordination service, not the source of truth.

# /etc/cron.daily/linkmarks-relay-backup
tar czf /var/backups/linkmarks-relay-$(date +%F).tar.gz \
    -C /var/lib linkmarks-relay
find /var/backups -name 'linkmarks-relay-*.tar.gz' -mtime +30 -delete

Monitoring

Three signals matter:

  1. Relay availabilityGET /healthz returns 200.
  2. Relay storagedu -sh /var/lib/linkmarks-relay.
  3. Sync activity — per-device, the number of records pushed and pulled.

A simple Prometheus exporter is documented in the linkmarks-relay repo (future).

Incident response

Stolen device

If a device is stolen:

  1. On every other device, run linkmarks sync push to flush any pending mutations.
  2. On the relay, rotate the bearer token for the stolen device (this invalidates its bearer token).
  3. The stolen device can no longer push or pull.
  4. The bookmarks are unaffected; the relay still has the latest bytes.

Relay compromise

If the relay is compromised:

  1. The attacker has opaque yrs bytes. Without the device-shared key (configured out-of-band), the bytes are unreadable.
  2. Rotate the bearer tokens on all devices.
  3. Provision a fresh relay (the bytes are reproducible from any device's local store).
  4. The compromise is contained.

Storage corruption

If the relay's storage is corrupted:

  1. Stop the relay.
  2. Restore from the most recent backup.
  3. Run linkmarks sync push from every device to reconcile.

Storage corruption on a single device does not propagate: the yrs merge applies only the diff, not a full state replacement.

Future hardening

  • TLS cert pinning for the client side, off by default in v2.2.0 (turning it on requires a one-time cert hash configuration).
  • Audit log at the relay level (who pushed what, when). Currently logged to stdout; the relay does not persist.
  • Rate limiting at the reverse proxy level. nginx limit_req is recommended for production deployments.

Reference

This page is the technical reference: env vars, file layout, config schema, exit codes, signal handling.

Environment variables

VariableDefaultDescription
LINKMARKS_CONFIG$XDG_CONFIG_HOME/linkmarks/config.tomlPath to the config file
LINKMARKS_STORE$XDG_DATA_HOME/linkmarks/linkmarks.dbPath to the SQLite store
LINKMARKS_RELAYfrom configOverride the relay URL
LINKMARKS_TOKENfrom configBearer token for the relay
LINKMARKS_THEMErustDefault TUI theme
LINKMARKS_NO_SYNCunsetSet to 1 to disable linkmarks sync
RUST_LOGinfoStandard env_logger filter
NO_COLORunsetIf set, TUI disables ANSI colors

The XDG_* defaults follow the XDG Base Directory Specification.

File layout

$XDG_CONFIG_HOME/linkmarks/
├── config.toml             # Main config
├── keymap.toml             # TUI keymap overrides (optional)
└── relay.toml              # Relay credentials (optional)

$XDG_DATA_HOME/linkmarks/
├── linkmarks.db            # SQLite store (single file)
├── linkmarks.db-wal        # Write-Ahead Log (SQLite-managed)
├── linkmarks.db-shm        # Shared memory (SQLite-managed)
└── sync/                   # yrs sub-document snapshots (preview)

$XDG_CACHE_HOME/linkmarks/
└── nucleo/                 # In-memory matcher cache (regenerated each session)

Config schema

The full schema lives in linkmarks-cli/src/config.rs. The config.toml.example file in the repo root shows every field with comments.

# ~/.config/linkmarks/config.toml

# SQLite store path (default: $XDG_DATA_HOME/linkmarks/linkmarks.db)
store = "/var/lib/linkmarks/linkmarks.db"

# Default sort mode for `linkmarks list` and the TUI
# One of: updated, title, canonical-url, created
default_sort = "updated"

# Default filter mode for the TUI
# One of: substring, tag, fuzzy
default_filter = "substring"

# Sync relay URL (used by `linkmarks sync`)
relay = "https://relay.example.com"

# Sync relay bearer token (read from $LINKMARKS_TOKEN if not set)
# Prefer the env var to avoid storing secrets on disk
token = "${LINKMARKS_TOKEN}"

# TUI theme (one of: rust, light, dark, ayu)
theme = "rust"

# Folder depth limit (default: 8)
max_folder_depth = 8

# Whether to keep separators from browser imports (default: false)
keep_separators = false

# Bridge-specific overrides
[bridges.chromium]
# Override the default ULID prefix for Chromium-imported bookmarks
# (default: "chr")
ulid_prefix = "chr"

[bridges.firefox]
# Include moz_annos by default (default: false)
with_annotations = false

[bridges.netscape]
# Preserve <DD> comments as notes (default: false)
dd_as_notes = false

Exit codes

CodeNameDescription
0SuccessCommand succeeded
1UserErrorInvalid flags, missing arguments
2StoreErrorSQLite open failed, schema mismatch
3ParseErrorBridge parser could not parse the input
4SyncErrorRelay unreachable, merge conflict
5PermissionErrorCannot read source, cannot write store
64ConfigErrorConfig file invalid
66NoInputExpected a TTY, got a pipe
73CantCreateCannot create a file or directory
130InterruptedSIGINT received (Ctrl+C)

The codes are stable across releases.

Signals

The CLI honours three signals:

SignalEffect
SIGINT (Ctrl+C)Graceful shutdown. The SQLite WAL is flushed and the connection is closed. Exit code 130.
SIGTERMSame as SIGINT. Used by systemd Type=oneshot services.
SIGHUPConfig reload. Re-reads config.toml without restart.

The TUI additionally honours SIGWINCH (terminal resize) for re-rendering.

File lock

A single advisory file lock at $XDG_DATA_HOME/linkmarks/.lock prevents two CLI instances from racing on the same store. The lock is released on graceful shutdown.

Schema migrations

The store has a schema_version table with a single row. Each migration is a linkmarks-core/src/migrations/NNNN_description.sql file. The migrations run automatically on linkmarks init and on every CLI invocation (in a single transaction).

The current schema version is 7.

Performance budgets

OperationTargetTested
linkmarks init< 100 ms12 ms (cold cache, ext4)
linkmarks import chromium (1000 records)< 5 s1.4 s
linkmarks dedupe (10,000 records)< 5 s1.9 s
linkmarks list --limit 100< 50 ms8 ms
linkmarks tui startup< 200 ms90 ms
linkmarks sync push (1000 changed records)< 3 s0.7 s
linkmarks sync pull (1000 changed records)< 3 s0.9 s

These budgets are enforced by the benchmark suite in linkmarks-bench-crdt.

Debugging

# Increase verbosity (can be repeated)
linkmarks -vv list

# Full debug logging to stderr
RUST_LOG=linkmarks_core=debug,linkmarks_cli=debug linkmarks list

# Profile a slow query
RUST_LOG=linkmarks_core::store=trace linkmarks dedupe

The TUI logs to a per-session file at $XDG_CACHE_HOME/linkmarks/tui-YYYY-MM-DD-HHMMSS.log (if the --log-file flag is passed).

License

LinkMarks is dual-licensed:

AGPL-3.0-or-later (open source) OR LicenseRef-Commercial (commercial).

You may choose which license applies to your use.

AGPL-3.0-or-later

The open-source license is the GNU Affero General Public License, version 3 or any later version published by the Free Software Foundation. The full text is at LICENSE in the repo root.

The AGPL §13 network-use clause is the meaningful restriction: if you run a modified version of LinkMarks as a network service, you must publish the modified source under the same license.

LicenseRef-Commercial

For entities that need to skip the AGPL §13 network-use clause without publishing their modifications, a commercial license is available. Contact opensource@loust.pro for terms.

The commercial license is what most enterprises use when they want to embed LinkMarks into a SaaS product without the AGPL disclosure obligation.

Third-party dependencies

LinkMarks depends on the following crates. Each is used under its own license:

CrateVersionLicensePurpose
rusqlite0.32MITSQLite bindings
clap4.5MIT/Apache-2.0CLI parser
ratatui0.28MITTUI framework
crossterm0.28MITTerminal I/O
nucleo0.5MITFuzzy matcher
yrs0.18MIT/Apache-2.0CRDT
tokio1.40MITAsync runtime (sync layer)
reqwest0.12MIT/Apache-2.0HTTP client (sync layer)
serde1.0MIT/Apache-2.0Serialization
serde_json1.0MIT/Apache-2.0JSON
toml0.8MIT/Apache-2.0Config parsing
ulid1.1MITULID generation
url2.5MIT/Apache-2.0URL parsing
chrono0.4MIT/Apache-2.0Timestamps
anyhow1.0MIT/Apache-2.0Error handling
thiserror1.0MIT/Apache-2.0Error types
tracing0.1MITStructured logging
tracing-subscriber0.3MITLog subscriber
env_logger0.11MIT/Apache-2.0Env-driven logger

The full transitive list is in Cargo.lock and the SBOM at docs/sbom.json in the repo root.

SPDX expression

The Cargo.toml metadata uses the SPDX expression:

License: AGPL-3.0-or-later OR LicenseRef-Commercial

The OR is intentional — the user picks one. See the SPDX spec for the formal semantics.

Contributing

Contributions are accepted under the same dual-license model. By submitting a pull request, you agree to license your contribution under both AGPL-3.0-or-later and LicenseRef-Commercial.

See CONTRIBUTING.md for the contribution workflow.

Trademark

"LinkMarks" is a project name. It is not a registered trademark. You may use the name to refer to the unmodified version of this project. You may not use the name to refer to a modified version without explicit written permission.

Contact

  • General questions: GitHub Discussions
  • Security disclosures: security@loust.pro
  • Commercial licensing: opensource@loust.pro