{"id":51742977,"url":"https://github.com/kkucherenkov/burkmak","last_synced_at":"2026-07-18T15:38:52.302Z","repository":{"id":371187084,"uuid":"1299716571","full_name":"kkucherenkov/burkmak","owner":"kkucherenkov","description":"Self-hosted read-it-later with native Kobo sync — save articles, read and highlight them anywhere, sync to your e-reader via OPDS or the Kobo store protocol.","archived":false,"fork":false,"pushed_at":"2026-07-15T21:56:23.000Z","size":3608,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-18T15:38:51.321Z","etag":null,"topics":["bookmarks","e-reader","flutter","kobo","nestjs","nuxt","opds","openapi","read-it-later","self-hosted"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kkucherenkov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-07-13T20:39:12.000Z","updated_at":"2026-07-15T21:52:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kkucherenkov/burkmak","commit_stats":null,"previous_names":["kkucherenkov/burkmak"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kkucherenkov/burkmak","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkucherenkov%2Fburkmak","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkucherenkov%2Fburkmak/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkucherenkov%2Fburkmak/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkucherenkov%2Fburkmak/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kkucherenkov","download_url":"https://codeload.github.com/kkucherenkov/burkmak/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kkucherenkov%2Fburkmak/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35622753,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-18T02:00:07.223Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bookmarks","e-reader","flutter","kobo","nestjs","nuxt","opds","openapi","read-it-later","self-hosted"],"created_at":"2026-07-18T15:38:51.632Z","updated_at":"2026-07-18T15:38:52.284Z","avatar_url":"https://github.com/kkucherenkov.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# burkmak\n\n**A quiet home for everything you mean to read.** Self-hosted, multi-user\nread-it-later: save a link from anywhere, read it in a clean reader, highlight\nwhat matters — then sync it to your Kobo and export your notes to Obsidian.\n\nburkmak is an Omnivore-style core with two differentiators built in from the\nroadmap up: **native Kobo sync** and **Obsidian export**. It runs on a small\nstack — NestJS + SQLite + Nuxt + Flutter — so a single binary-shaped deployment\nserves the web app, the mobile app, and your e-reader.\n\n\u003e Status: **P1–P6 shipped** — save/organise, extraction/reader/highlights/\n\u003e full-text search, capture surfaces, Kobo (OPDS **and native store-protocol\n\u003e sync with reading-state write-back**), Obsidian export, dark theme, and\n\u003e **auto-extraction on save**. One honest ceiling remains: native sync is\n\u003e verified against a full protocol simulation, with the physical-device pass\n\u003e still pending (needs HTTPS in front). See the [roadmap](#roadmap).\n\n## Screenshots\n\n|                                                                         |                                                                     |\n| ----------------------------------------------------------------------- | ------------------------------------------------------------------- |\n| **Library**                                                             | **Reader**                                                          |\n| ![Library — saved items, filters, tags](assets/screenshots/library.png) | ![Clean reader view with highlights](assets/screenshots/reader.png) |\n| **Save from anywhere**                                                  | **Dark theme**                                                      |\n| ![Quick save via the /save capture page](assets/screenshots/save.png)   | ![Library in dark mode](assets/screenshots/library-dark.png)        |\n\n\u003csub\u003eLive screenshots of the running app (seeded demo library).\u003c/sub\u003e\n\n## Features\n\n### Save \u0026 organise — _shipped (S1)_\n\n- Save a URL from the web add-bar, the mobile app, a share-sheet, or a\n  bookmarklet — every surface hits the same `POST /items`.\n- Live metadata: title, site, excerpt, favicon and lead image fill in **without\n  a refresh**, pushed over Server-Sent Events as a background job resolves them.\n- **Auto-extraction on save** — every saved article is extracted in the\n  background automatically, so the reader view, OPDS feed, and Kobo sync are\n  ready without another tap (pre-existing libraries are backfilled once).\n- Tags, read-state (`unread` / `read` / `archived`), and favorites.\n- Filtered, searchable list. **Per-user libraries** — you never see another\n  user's data.\n- **Light \u0026 dark themes** — a one-tap toggle in the header plus a\n  system/light/dark preference in Settings.\n\n### Read \u0026 highlight — _shipped (S2)_\n\n- **Automatic full-article extraction** (Readability + sanitisation) into a\n  clean, ad-free reader view — kept even if the original goes dark. A manual re-extract button covers pages that need a retry.\n- Locally-cached article images (SSRF-guarded), served from your own server.\n- **Full-text search (SQLite FTS5)** across title, URL, **and** article body —\n  same search box, now matches the text inside your saved pieces.\n- **Highlights \u0026 notes**: select text, pick a colour, attach a note. Authored\n  on web, rendered read-only on mobile.\n\n### Capture anywhere — _shipped (S3)_\n\n- **Android share-sheet** target — share a link from any app straight into your\n  library (auto-saves; signed-out shares are held and saved after login). iOS\n  share extension is a tracked follow-up.\n- Desktop **browser bookmarklet** — one click saves the current page via a\n  same-origin `/save` popup that reuses your session.\n- Spec: [`specs/features/2026-06-14-capture-surfaces.md`](specs/features/2026-06-14-capture-surfaces.md).\n\n### Sync \u0026 export — _shipped (S4 · S5 · P6)_\n\n- **Kobo sync** (S4 + P6) — two tiers, both served straight from your library,\n  no firmware hacking:\n  - **OPDS catalog** (`GET /api/v1/opds`) with covers, cursor pagination, and\n    OpenSearch, plus EPUB/KEPUB generation\n    (`GET /api/v1/items/{id}/epub`). Add it on a stock Kobo via Settings →\n    \"Add an OPDS catalog\".\n  - **Native store-protocol sync** (`/api/v1/kobo/{token}/*` — the token\n    rides in the URL path because the Kobo's browser can't send auth\n    headers): the device pulls new articles automatically on connect, and\n    reading state writes back (finish a piece on the Kobo → it's `read` in\n    burkmak; archive in the app → it disappears from the device). Point\n    `api_endpoint` in `Kobo eReader.conf` at\n    `https://\u003cyour-host\u003e/api/v1/kobo/\u003cPAT\u003e` — HTTPS required by the device.\n    _Verified against a full store-protocol simulation; the physical-device\n    pass is the one remaining ceiling._\n- **Obsidian export** (S5): a markdown **export API** (one note per article —\n  source, metadata, highlights, notes; idempotent via a `burkmakId` frontmatter\n  key) + an **[Obsidian plugin](packages/obsidian-plugin)** that syncs it into\n  your vault.\n- **Personal access tokens** (Settings): a long-lived token for those headless\n  clients — Kobo's OPDS reader (HTTP Basic) and the Obsidian plugin (Bearer).\n\n## Roadmap\n\nPhase order: **S0 → S1 → S2**, then **S3 / S4 / S5** in parallel, then a **P6** polish wave. Each phase is\none spec → plan → build cycle. Full detail in [`specs/roadmap.md`](specs/roadmap.md)\nand the requirements in [`specs/PRD.md`](specs/PRD.md).\n\n| Phase  | Subsystem | Ships                                                                                                                         | Status     |\n| ------ | --------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------- |\n| **P1** | S0 + S1   | Foundation (SQLite, jobs + SSE spine) and core library                                                                        | ✅ shipped |\n| **P2** | S2        | Article extraction, reader view, FTS5 body search, highlights                                                                 | ✅ shipped |\n| **P3** | S3        | Android share-sheet capture, browser bookmarklet                                                                              | ✅ shipped |\n| **P4** | S4        | OPDS catalog + EPUB/KEPUB download                                                                                            | ✅ shipped |\n| **P5** | S5        | Obsidian export API + Obsidian plugin                                                                                         | ✅ shipped |\n| **P6** | polish    | Native Kobo store sync + read-state write-back · OPDS covers/pagination/search · dark theme + switcher · auto-extract on save | ✅ shipped |\n\n## Architecture\n\n```mermaid\nflowchart LR\n  subgraph clients[\" \"]\n    web[\"web\u003cbr/\u003e\u003csub\u003eNuxt 4 SPA · :3001\u003c/sub\u003e\"]\n    mobile[\"mobile\u003cbr/\u003e\u003csub\u003eFlutter 3.41\u003c/sub\u003e\"]\n    kobo[\"Kobo e-reader\u003cbr/\u003e\u003csub\u003estock device\u003c/sub\u003e\"]\n  end\n\n  subgraph backend_group[\"apps/backend · :3000/api/v1\"]\n    api[\"NestJS 11 + CQRS\u003cbr/\u003eBetter Auth\"]\n    jobs[\"DB-backed job worker\u003cbr/\u003e\u003csub\u003efetch_metadata · extract_article\u003c/sub\u003e\"]\n    sqlite[(\"SQLite\u003cbr/\u003ePrisma 7 + FTS5\")]\n  end\n\n  subgraph contracts[\"packages/specs — single source of truth\"]\n    openapi[\"openapi.yaml\"]\n  end\n\n  web --\u003e|\"@app/api-client-ts\"| api\n  mobile --\u003e|\"app_api_client (dart)\"| api\n  kobo --\u003e|\"OPDS + Kobo store API\"| api\n  api --\u003e sqlite\n  api --\u003e jobs\n  jobs --\u003e sqlite\n  api -.-\u003e|\"Server-Sent Events\"| web\n  api -.-\u003e|\"Server-Sent Events\"| mobile\n  openapi -. \"codegen\" .-\u003e web\n  openapi -. \"codegen\" .-\u003e mobile\n  openapi -. \"validates\" .-\u003e api\n```\n\n- **Spec-first.** `packages/specs/openapi/openapi.yaml` is the source of truth.\n  It generates the TypeScript and Dart clients and is enforced at runtime by\n  `express-openapi-validator` — the backend physically cannot serve an\n  undocumented route.\n- **No external services for the core.** SQLite (with FTS5) is the database; a\n  DB-backed worker runs background jobs; realtime is plain SSE. The local stack\n  is just two containers (`backend`, `web`).\n\n### Stack\n\n| Layer         | Tech                                                                                              |\n| ------------- | ------------------------------------------------------------------------------------------------- |\n| **Backend**   | NestJS 11 · Prisma 7 (SQLite + FTS5) · CQRS · Better Auth · DB job worker + SSE · RFC 9457 errors |\n| **Web**       | Nuxt 4 (SPA, `ssr:false`) · Nuxt UI v4 · Tailwind 4 · `@app/ui` · `@nuxtjs/i18n` (en/ru)          |\n| **Mobile**    | Flutter 3.41 · flutter_bloc · get_it · Dio · flutter_secure_storage · slang (en/ru/uk/el)         |\n| **Contracts** | OpenAPI 3.1 + AsyncAPI 3.0 → generated `api-client-{ts,dart}` (read-only, codegen-owned)          |\n| **Design**    | W3C design tokens → CSS / TS / Dart · `@app/ui` Vue components + colocated Storybook              |\n\n## Repository layout\n\n```\napps/\n  backend/          NestJS 11 + Prisma 7 (SQLite) + CQRS + Better Auth\n  web/              Nuxt 4 (SPA) + Nuxt UI v4 + Tailwind v4 + @app/ui\n  mobile/           Flutter 3.41 + flutter_bloc + get_it + Dio\npackages/\n  specs/            OpenAPI 3.1 + AsyncAPI 3.0 — single source of API truth\n  api-client-ts/    generated TS client — never edit by hand\n  api-client-dart/  generated Dart client — never edit by hand\n  ui/               @app/ui Vue components + Storybook + specs\n  ui_flutter/       app_ui Flutter widgets + theme\n  design-tokens/    tokens pipeline: JSON → CSS / TS / Dart\n  obsidian-plugin/  Obsidian plugin (esbuild → main.js) for the export API\nspecs/\n  PRD.md            product requirements\n  roadmap.md        phase → subsystem map\n  HANDOFF.md        cold-start orientation\n  features/         per-feature specs + implementation plans\n  design/           tokens JSON + mockups + component inventory\n  tasks/            active.md (work stack) + done.md (archive)\ndocker/             compose.yml (backend + web) + service configs\n.claude/            project rules (CLAUDE.md), domain handbook, subagent roster\n```\n\n## Quick start\n\n```sh\ngit clone \u003cthis-repo\u003e burkmak\ncd burkmak\npnpm install\npnpm spec:codegen                       # generate TS + Dart API clients from the spec\npnpm design:build                       # generate CSS / TS / Dart design tokens\ndocker compose -f docker/compose.yml up -d\ncurl localhost:3000/api/v1/health       # → {\"status\":\"ok\",...}\n```\n\nOpen **http://localhost:3001** for the web app. Containers mount the repo as a\nvolume, so edits hot-reload — don't also run `pnpm dev` against the same ports.\n\nFor the mobile app: `cd apps/mobile \u0026\u0026 flutter run` (Flutter 3.41 + Dart 3.8).\n\n### Deploy on your homelab\n\nEvery release publishes ready-to-run Docker images (amd64 + arm64) to GitHub\nPackages — no clone or toolchain needed. Install with a single\n`docker compose up -d`: see [`deploy/README.md`](deploy/README.md).\n\n### Requirements\n\n- Node.js ≥ 24 · pnpm ≥ 10\n- Docker + Docker Compose\n- Flutter 3.41 + Dart 3.8 _(only if building mobile)_\n\n## How this project is built\n\nburkmak is developed spec-first with [Claude Code](https://claude.com/claude-code),\nand the repo carries the guardrails that keep an agentic workflow honest:\n\n- **Codegen drift guard** — CI regenerates every client from the spec and fails\n  on any diff. Generated clients are never hand-edited.\n- **Runtime contract validation** — `express-openapi-validator` rejects requests\n  that aren't in the OpenAPI spec.\n- **Migrations-only database** — every schema change is a committed Prisma\n  migration; `prisma db push` is prohibited.\n- **Design-token discipline** — Stylelint bans hex literals and inline styles in\n  brand components; every colour comes from a token.\n- **In-repo task stack** — work is pushed onto\n  [`specs/tasks/active.md`](specs/tasks/active.md) and archived to\n  [`done.md`](specs/tasks/done.md), not lost in a chat window.\n\n## Documentation\n\n| Topic                                     | File                                                             |\n| ----------------------------------------- | ---------------------------------------------------------------- |\n| Product requirements                      | [`specs/PRD.md`](specs/PRD.md)                                   |\n| Roadmap (phases → subsystems)             | [`specs/roadmap.md`](specs/roadmap.md)                           |\n| Cold-start orientation                    | [`specs/HANDOFF.md`](specs/HANDOFF.md)                           |\n| Feature specs + implementation plans      | [`specs/features/`](specs/features)                              |\n| Design system, tokens, mockups            | [`specs/design/README.md`](specs/design/README.md)               |\n| Backend · CQRS · Prisma · API conventions | [`.claude/docs/handbook.md`](.claude/docs/handbook.md)           |\n| Design system · `@app/ui` · tokens · BEM  | [`.claude/docs/design-system.md`](.claude/docs/design-system.md) |\n| i18n (web + mobile + backend)             | [`.claude/docs/i18n.md`](.claude/docs/i18n.md)                   |\n| Testing pyramid · DoD · PR checklist      | [`.claude/docs/testing.md`](.claude/docs/testing.md)             |\n| Security · observability · a11y · perf    | [`.claude/docs/security.md`](.claude/docs/security.md)           |\n| Project rules (loaded by Claude Code)     | [`.claude/CLAUDE.md`](.claude/CLAUDE.md)                         |\n\n## Contributing \u0026 security\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).\n\n## License\n\n[MIT](LICENSE).\n\n## Acknowledgements\n\nBuilt on NestJS, Nuxt, Flutter, Prisma, Better Auth, Redocly, AsyncAPI, slang,\nTailwind, Storybook, Turbo, and pnpm. Workflow shaped for\n[Claude Code](https://claude.com/claude-code).\n\u003c/content\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkkucherenkov%2Fburkmak","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkkucherenkov%2Fburkmak","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkkucherenkov%2Fburkmak/lists"}