Serverless cross-browser bookmark sync. Bookmarks live as a JSON file in your own private GitHub repo; browser extensions and a web UI both talk directly to the GitHub Contents API. No server, no backend, no infrastructure to host. You own your data — it's just a file in a repo you control.
Status: Chrome extension is functional end-to-end (save via toolbar
button, two-way sync with the native bookmark tree, 5-min poll for remote
changes, automatic conflict retry). Firefox MV3 add-on shipping the same
source as Chrome via a shared package. Safari / web UI are next in
the roadmap. See spec.md for the full design.
- Save the current tab to GitHub via a toolbar button
- Drag a URL to your Chrome bookmarks bar → it appears in
bookmarks.jsonon GitHub within ~1 second - Edit a bookmark's title in Chrome → updates remote within ~1 second
- Delete a bookmark in Chrome → soft-deleted (tombstoned) remotely; garbage-collected from the JSON after 30 days but retained in git history forever
- Edit
bookmarks.jsondirectly on GitHub → changes pull into Chrome on the next 5-minute poll - Concurrent edits from multiple devices reconcile automatically via GitHub's file SHA + optimistic retry-replay
- 162 automated tests (unit + Playwright e2e against real Chromium)
- Optional tracking-param stripping (utm_*, fbclid, gclid, etc.) at save time — opt-in via settings
| Package | Role |
|---|---|
@gitmarks/core |
Shared TypeScript library: schemas (Zod), GitHub Contents API client with optimistic concurrency, ULID + URL helpers, pure mutation helpers |
@gitmarks/extension-shared |
Cross-browser extension source — popup, options, background, lib/ helpers. Consumed by both browser shells via workspace:*. 96 unit tests live here. |
@gitmarks/extension-chrome |
Chrome MV3 shell. Manifest + Vite/crxjs build + Playwright e2e. Thin entry files import from extension-shared. |
@gitmarks/extension-firefox |
Firefox MV3 shell. Manifest + plain Vite build. Same source as Chrome via extension-shared. Load via about:debugging. |
pnpm install
pnpm --filter @gitmarks/extension-chrome buildThen in Chrome:
chrome://extensions/→ toggle Developer mode on- Load unpacked → select
packages/extension-chrome/dist/ - Click the toolbar icon → "Set up gitmarks"
- Paste a fine-grained PAT (Contents: read/write scope on your bookmarks repo), enter owner/repo/branch, click Save
See packages/extension-chrome/README.md for the full setup walkthrough,
the manual smoke test checklist, and architecture notes.
- The repo must be private. Public repo + the project name = anyone can find your bookmarks. The extension does NOT enforce this — it's on you when you create the repo on github.com.
- Use a fine-grained PAT scoped to only your bookmarks repo with only Contents: read/write. Never use a classic PAT or one with broader scopes — if your browser profile is ever exfiltrated, that token only unlocks your bookmarks, not your whole GitHub account.
- The PAT is stored in
chrome.storage.local, which is origin-scoped (other extensions / sites can't read it) but readable by anyone with access to your unlocked browser profile. Treat it like a saved password. - No telemetry. The extension only talks to
api.github.com. That's enforced by the MV3 manifest'shost_permissions.
# Everything
pnpm install
pnpm test # all unit tests across packages
pnpm typecheck
pnpm build
# Just one package
pnpm --filter @gitmarks/core test
pnpm --filter @gitmarks/extension-shared test # all extension unit tests live here
pnpm --filter @gitmarks/extension-chrome e2e # Playwright + real ChromiumThe repo is a pnpm workspace monorepo. Each package has its own
README.md with package-specific docs.
[Chrome ext] [Firefox ext] [Safari ext (planned)] [Web UI (planned)]
\ | / /
\ | / /
v v v v
GitHub REST API (api.github.com)
|
v
User's private repo: bookmarks.json + tags.json
The load-bearing invariants:
- No server, ever. Clients talk to GitHub REST API directly. PAT
lives client-side (
chrome.storage.local). - Optimistic concurrency via GitHub file SHA. On 409, the core client refetches and replays the mutation (up to 3 attempts with exponential backoff).
- Eventual consistency, ~30s target. Event-driven push for local
changes (500ms debounce). 5-minute poll for remote changes via
chrome.alarms, with ETag conditional reads so unchanged polls cost nothing against the rate limit. - Soft deletes (tombstones) for ~30 days; git history retains everything forever.
- Suppression registry prevents loop-back: when the extension applies
a remote change to
chrome.bookmarks, the affected URL is parked in an in-memory registry for ~2 seconds so the resulting local event doesn't echo back to GitHub.
- ✅
@gitmarks/core— schemas, GitHub client, mutations - ✅ Chrome MVP — toolbar-button save flow
- ✅ Chrome native tree integration — listeners, reconcile, poll loop
- ✅ Tracking-param stripping (opt-in)
- ✅ Firefox MV3 add-on (#23)
- ⬜ Web UI v1: list + search + tag management (#24)
- ⬜ Web UI v2: bulk operations + trash + export (#25)
- ⬜ Safari (#26)
spec.md— full design spec (source of truth for non-obvious decisions)CONTRIBUTING.md— branch/PR conventions, TDD policy, plan-driven workflowCLAUDE.md— guidance for AI agents working in this repoLICENSE— MITdocs/superpowers/plans/— implementation plans, one per branchpackages/*/README.md— package-specific documentationexamples/example-bookmarks-repo/— samplebookmarks.json+tags.jsonto seed a fresh repo, used by@gitmarks/corefixture tests.github/workflows/test.yml— CI (typecheck + unit tests + build on every PR)
See CONTRIBUTING.md for the branch/PR conventions, conventional-commit
scopes, and the plan-driven workflow used for larger features. Every
change goes through a PR with green CI — no direct commits to main.