A Chrome extension (MV3) that captures bookmarks and sends them to a local Bun/Elysia API backed by MariaDB.
Everything is implemented in JavaScript (no TypeScript) for the extension; the API uses TypeScript with Bun.
- Stack
- Quick Start
- Scripts
- Repository Structure
- API Overview
- Chrome Extension
- Backup
- Troubleshooting
- License
| Layer | Technology |
|---|---|
| Chrome Extension | Manifest V3, vanilla JS/HTML/CSS |
| API | Bun + Elysia + TypeScript |
| ORM | Drizzle |
| Database | MariaDB 11 |
| Infrastructure | Rootless Podman + systemd (Quadlet) |
cp api/.env.example api/.env
# Edit api/.env — set DB_PASSWORD, MARIADB_PASSWORD, MARIADB_ROOT_PASSWORD
nano api/.env./scripts/install.shinstall.sh will:
- Copy
api/.env.example → api/.envif missing (then exit so you can set passwords) - Pull
mariadb:11andphpmyadmin:5 - Build
localhost/bookmark-api:latest - Copy
quadlet/unit files into~/.config/containers/systemd/ - Run
systemctl --user daemon-reloadand start the pod
curl http://localhost:11650/health
# → {"status":"ok"}| Service | URL |
|---|---|
| API | http://localhost:11650 |
| Swagger UI | http://localhost:11650/docs |
| OpenAPI JSON | http://localhost:11650/openapi.json |
| Bookmark viewer | http://localhost:11650/app |
| Category manager | http://localhost:11650/categories |
| phpMyAdmin | http://localhost:11651 |
- Open
chrome://extensions - Enable Developer mode
- Click Load unpacked → select the
extension/folder - The extension defaults to
http://localhost:11650— change it in Options if needed
All scripts live in scripts/ and are run from the repo root.
| Script | Description |
|---|---|
./scripts/install.sh |
Full install: build image, deploy Quadlet files, start pod |
./scripts/uninstall.sh |
Stop services, remove Quadlet files, optionally remove image + data |
./scripts/rebuild.sh |
Rebuild API image and restart bookmark-api.service |
./scripts/start.sh |
Start the pod (all services) |
./scripts/stop.sh |
Stop the pod (all services) |
./scripts/restart.sh [api|db|pma] |
Restart one service or the whole pod |
./scripts/logs.sh [api|db|pma|all] |
Tail logs — defaults to api |
./scripts/status.sh |
Show systemctl --user status for all services |
./scripts/dev.sh |
Run API locally via bun run dev (no container) |
./scripts/backup.sh |
Dump MariaDB to backups/bookmark_YYYY-MM-DD_HHMMSS.sql.gz |
See scripts/README.md for full per-script documentation.
bookmarkManager/
├── api/ # Bun/Elysia API
│ ├── src/
│ │ ├── server.ts # Elysia app + routes
│ │ ├── db/ # Drizzle schema, client, migrations
│ │ ├── ui/ # Served HTML pages (/app, /categories)
│ │ └── smoke/ # No-DB health smoke test
│ ├── Dockerfile
│ ├── drizzle.config.ts
│ ├── package.json
│ ├── .env # Live credentials (gitignored)
│ └── .env.example # Template — copy to .env
├── extension/ # Chrome MV3 extension (vanilla JS)
│ ├── manifest.json
│ ├── popup/
│ ├── background/
│ ├── options/
│ ├── lib/
│ └── assets/
├── quadlet/ # Canonical Quadlet unit files (version-controlled)
│ ├── bookmark.pod
│ ├── bookmark-api.container
│ ├── bookmark-db.container
│ └── bookmark-pma.container
├── scripts/ # Lifecycle scripts
│ ├── install.sh
│ ├── uninstall.sh
│ ├── rebuild.sh
│ ├── start.sh
│ ├── stop.sh
│ ├── restart.sh
│ ├── logs.sh
│ ├── status.sh
│ ├── dev.sh
│ └── backup.sh # DB dump → backups/
├── backups/ # Dump files (gitignored)
├── PLAN.md # Full project reference
└── README.md # This file
Interactive docs always available at http://localhost:11650/docs.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Redirect to /app |
GET |
/health |
Returns { status: "ok" } |
GET |
/docs |
Swagger UI |
GET |
/openapi.json |
OpenAPI spec |
GET |
/app |
Bookmark viewer UI |
GET |
/categories |
Category management UI |
GET |
/flag-counts |
Count of active bookmarks per flag |
GET |
/backup |
Download a gzipped MariaDB dump (requires ?token=) |
| Method | Path | Description |
|---|---|---|
GET |
/bookmarks |
List/filter bookmarks (?limit=&offset=&classificationId=&tagId=&flag=&sortBy=&archived=) |
POST |
/bookmarks |
Create bookmark |
PATCH |
/bookmarks/:id |
Edit title, description, flags, tags, classifications |
PATCH |
/bookmarks/:id/archive |
Soft-delete (sets archivedAt) |
PATCH |
/bookmarks/:id/restore |
Restore archived bookmark |
| Method | Path | Description |
|---|---|---|
GET |
/tags |
List/search tags (?query=&limit=&offset=&sort=) |
POST |
/tags |
Create tag |
PATCH |
/tags/:id/archive |
Archive tag |
PATCH |
/tags/:id/restore |
Restore archived tag |
| Method | Path | Description |
|---|---|---|
GET |
/classifications |
All active classifications, nested by group |
POST |
/classifications |
Create classification (optionally with new group) |
PATCH |
/classifications/:id |
Rename classification |
PATCH |
/classifications/:id/reorder |
Set display order |
PATCH |
/classifications/:id/archive |
Archive classification |
PATCH |
/classifications/:id/restore |
Restore archived classification |
| Method | Path | Description |
|---|---|---|
GET |
/classifications/groups |
List groups with nested classifications (management view) |
POST |
/classifications/groups |
Create group |
PATCH |
/classifications/groups/:id |
Rename group |
PATCH |
/classifications/groups/:id/reorder |
Set display order |
PATCH |
/classifications/groups/:id/archive |
Archive group |
PATCH |
/classifications/groups/:id/restore |
Restore archived group |
Data lifecycle: nothing is hard-deleted. All "delete" actions set archivedAt and can be reversed with the corresponding /restore endpoint.
Duplicate URL detection: POST /bookmarks returns 409 with existing bookmark metadata. Pass allowDuplicate: true to save anyway after explicit user confirmation.
- Click the action icon for the popup (full save with form)
- Right-click a page for context menus: Quick Save / Full Save
See extension/README.md for full extension documentation.
See api/README.md for full API documentation.
Two backup methods are available: a shell script (recommended for cron/automation) and an in-browser API endpoint.
./scripts/backup.sh- Runs
mariadb-dumpinside thebookmark-dbcontainer viapodman exec - Pipes output through
gzip -9 - Saves to
backups/bookmark_YYYY-MM-DD_HHMMSS.sql.gz(directory is gitignored)
See scripts/README.md for full options and a restore command.
GET http://localhost:11650/backup?token=<BACKUP_TOKEN>
- Returns a
bookmark_YYYY-MM-DD_HHMMSS.sql.gzdownload - Requires
BACKUP_TOKENto be set inapi/.env(a strong random value — the defaultchange_me_pleaseis refused with503) - Returns
401on token mismatch,503if token is unconfigured - The ⬇ Backup button in the
http://localhost:11650/apptopbar calls this endpoint — it will prompt for your token
gunzip -c backups/bookmark_YYYY-MM-DD_HHMMSS.sql.gz \
| podman exec -i bookmark-db mariadb -u bookmarks -p bookmarks| Symptom | Fix |
|---|---|
| Port 11650 or 11651 busy | Edit quadlet/bookmark.pod, change PublishPort, re-run ./scripts/install.sh |
| API can't reach DB | Check api/.env — DB_PASSWORD must match MARIADB_PASSWORD; DB_HOST must be 127.0.0.1 |
| Extension can't reach API | Verify base URL in Options; check host_permissions in extension/manifest.json |
| Services not starting at boot | Run loginctl enable-linger $USER to enable linger for your user |
This project is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).
https://creativecommons.org/licenses/by-sa/4.0/
© 2026 Jaco Steyn — Licensed under CC BY-SA 4.0

