{"id":50418928,"url":"https://github.com/chenyukang/obr","last_synced_at":"2026-05-31T07:30:35.528Z","repository":{"id":359963289,"uuid":"1243172304","full_name":"chenyukang/obr","owner":"chenyukang","description":"A local-first web companion for capturing, editing, and reading Obsidian notes from any browser.","archived":false,"fork":false,"pushed_at":"2026-05-24T10:06:20.000Z","size":975,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-24T11:31:48.305Z","etag":null,"topics":["obsidian","rust","web"],"latest_commit_sha":null,"homepage":"","language":"Rust","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/chenyukang.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":null,"dco":null,"cla":null}},"created_at":"2026-05-19T05:39:14.000Z","updated_at":"2026-05-24T10:06:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chenyukang/obr","commit_stats":null,"previous_names":["chenyukang/obr"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/chenyukang/obr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenyukang%2Fobr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenyukang%2Fobr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenyukang%2Fobr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenyukang%2Fobr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chenyukang","download_url":"https://codeload.github.com/chenyukang/obr/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chenyukang%2Fobr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33723548,"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-05-31T02:00:06.040Z","response_time":95,"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":["obsidian","rust","web"],"created_at":"2026-05-31T07:30:34.803Z","updated_at":"2026-05-31T07:30:35.523Z","avatar_url":"https://github.com/chenyukang.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Obr\n\nObr is a local-first web companion for an Obsidian vault. It gives you a fast\nbrowser interface for capturing daily notes, editing Markdown blocks, reading\nand searching notes, managing todos, and uploading images, while keeping your\ncontent as plain Markdown files in the vault you already use.\n\nObr runs on your own machine and can be opened locally or exposed to trusted\ndevices through a stable HTTPS origin such as Tailscale Serve/Funnel. It is\nbuilt for personal writing workflows where the browser is the quick input\nsurface and Obsidian remains the durable knowledge base.\n\n## Setup\n\nInstall Rust stable, then clone and build:\n\n```bash\ncargo build --release\n```\n\nFor the fastest setup, let Obr generate `config/local.toml`, prepare the vault\ndirectory, start the daemon, and print the service URL:\n\n```bash\n./target/release/obr init --vault /path/to/obsidian/vault\n```\n\nTo also publish Obr through Tailscale Funnel:\n\n```bash\n./target/release/obr init --vault /path/to/obsidian/vault --tailscale\n```\n\nThe Tailscale flow starts a separate userspace `tailscaled` under\n`$HOME/.local/share/tailscale-obr`, asks you to approve the Tailscale login URL\nif needed, enables Funnel, writes the resulting `*.ts.net` hostname into\n`config/local.toml`, starts Obr, and prints both the local and public URLs. Use\n`--hostname \u003cname\u003e` to request a specific Tailscale node name; the actual\npublished hostname is read back from `tailscale funnel status`.\n\nIf `config/local.toml` already exists, `obr init` backs it up before writing the\nnew config.\n\nFor manual setup, create a local config:\n\n```bash\ncp config.example.toml config/local.toml\n```\n\nPoint `vault_path` at your Obsidian vault. You can either edit `config/local.toml` directly:\n\n```toml\nvault_path = \"/path/to/obsidian/vault\"\n```\n\nor keep the default `vault_path = \"vault\"` and create a symlink:\n\n```bash\nln -s /path/to/obsidian/vault vault\n```\n\nTo try Obr without touching a real vault, copy the demo vault:\n\n```bash\ncp -R examples/vault vault\n```\n\nBefore running Obr, generate a password hash and add it to `config/local.toml`\nas shown in the Password section below.\n\n## Vault Layout\n\nObr keeps vault-specific paths configurable so it can fit different Obsidian layouts:\n\n```toml\n# Daily memo files are created as \u003cdaily_dir\u003e/\u003cYYYY-MM-DD\u003e.md.\ndaily_dir = \"Daily\"\n\n# Named quick-entry pages are created under this directory, for example\n# page = \"project/foo\" writes \u003centry_dir\u003e/project/foo.md.\nentry_dir = \"Posts\"\n\n# Uploaded images are stored here and served through /images/* and /image-preview/*.\nimage_dir = \"Pics\"\n\n# The Todo view and page = \"todo\" entries use this file.\ntodo_path = \"Posts/todo.md\"\n\n# RSS detail annotations are saved here.\nannotation_dir = \"annotations\"\n```\n\nThese vault layout paths are relative to `vault_path`. Parent path components such as `..` are rejected.\n\nRuntime cache data is separate from the vault and is written under the gitignored `data` directory in the process working directory.\n\n## Appearance\n\nObr has a dark-mode toggle in the top-right toolbar. The button switches the\ncurrent appearance and can return to automatic mode; the manual choice is stored\nin the browser.\n\nTo make automatic mode switch at a fixed local time window, configure both\nvalues in `config/local.toml`:\n\n```toml\ndark_mode_start = \"21:00\"\ndark_mode_end = \"07:00\"\n```\n\nTimes are interpreted by the browser using its system timezone. Overnight\nranges are supported.\n\n## RSS Reader\n\nObr can maintain a local RSS reading list. Enable it in `config/local.toml`:\n\n```toml\nrss_enabled = true\nrss_feeds_path = \"Zero/feeds.md\"\nrss_data_dir = \"data/rss\"\nrss_refresh_minutes = 30\nrss_max_items_per_feed = 20\nrss_fetch_full_content = true\nrss_ai_summary_enabled = true\nrss_ai_full_translation_enabled = false\nrss_ai_summary_chars = 200\n# Optional: enables Chinese summaries for newly fetched non-Chinese posts.\ndeepseek_api_key = \"sk-...\"\ndeepseek_api_base = \"https://api.deepseek.com\"\ndeepseek_model = \"deepseek-v4-flash\"\nrss_ai_translation_provider = \"deepseek\"\n# tencent_secret_id = \"AKID...\"\n# tencent_secret_key = \"...\"\ntencent_translate_endpoint = \"https://tmt.tencentcloudapi.com\"\ntencent_translate_region = \"ap-guangzhou\"\ntencent_translate_source = \"en\"\ntencent_translate_target = \"zh\"\ntencent_translate_project_id = 0\ntencent_translate_max_chars = 1800\n```\n\n`rss_feeds_path` is relative to `vault_path` and should contain one RSS, Atom,\nor JSON Feed URL per line. Blank lines and `#` comments are ignored, and\nduplicate URLs are skipped.\n\nRSS metadata and read/unread state are stored in `data/rss/rss.sqlite`. Article\nMarkdown is stored under `data/rss/content/`. When\n`rss_fetch_full_content = true`, Obr fetches article pages and uses\n`rs-trafilatura` to extract readable Markdown. If extraction fails, it falls\nback to feed content or summary. Each refresh treats the feeds file as the\nsource of truth: removing a feed URL from the file removes that feed's stored\nitems and article Markdown on the next scan. The RSS detail page also has an\nUnsubscribe action, which removes the feed URL from `rss_feeds_path` and prunes\nthat feed's cached items immediately.\n\nIf `deepseek_api_key` is configured and `rss_ai_summary_enabled = true`, newly\nfetched non-Chinese posts are sent to the configured OpenAI-compatible chat API\nfor an automatic Chinese summary. `rss_ai_summary_chars` is a soft target for\nthe prompt, not a hard server-side truncation limit; the default asks for about\n200 Chinese characters and allows the model to stay natural. Existing items are\nnot summarized again during ordinary refreshes. Full-text translation is off by\ndefault to avoid surprise API cost. Set `rss_ai_full_translation_enabled = true`\nto also request and store full Chinese translations during RSS refresh.\n\n`rss_ai_translation_provider` controls full-text translation. `deepseek` reuses\nthe configured chat API and stores the model's bilingual Markdown. `tencent`\nuses Tencent Cloud Machine Translation and requires `tencent_secret_id` plus\n`tencent_secret_key` in your private `config/local.toml`; `config.example.toml`\nshould keep only placeholders. Tencent translation stores the same bilingual\nMarkdown shape as the DeepSeek path: each original block followed by a quoted\nChinese translation. The RSS detail page's manual Translate action is available\nwhen the selected translation provider is configured.\n\nRSS detail annotations are saved as Markdown under `annotation_dir`, which\ndefaults to `annotations`. Each RSS post gets one annotation file and additional\nnotes for the same post are appended to that file.\n\n## Security Model\n\nObr is designed as a local-first personal app. It can be exposed to a phone or a\nremote browser, but vault content, uploaded images, page drafts, cached pages,\npasskeys, logs, and sync outbox data should all be treated as sensitive local\ndata.\n\nKeep these paths out of Git history:\n\n```text\nconfig/local.toml\nvault\ndata\nlogs\ncache\n```\n\nWhen Obr is reachable outside the local machine, serve it through HTTPS, set\n`secure_cookies = true`, and configure a stable `webauthn_rp_id`. Obr derives\nthe WebAuthn origin as `https://\u003cwebauthn_rp_id\u003e` unless `webauthn_origin` is\nset explicitly. Obr validates request `Host` headers and rejects browser\ncross-site write requests with untrusted `Origin` or `Sec-Fetch-Site` headers.\nAvoid exposing Obr directly to the public internet without an additional\ntrusted access-control layer.\n\n## Password\n\nGenerate an Argon2 password hash:\n\n```bash\n./target/release/obr hash-password\n```\n\nThe command prompts for the password twice without echoing it, then prints a line you can put in `config/local.toml`:\n\n```toml\nusername = \"admin\"\npassword_hash = \"$argon2id$...\"\nallow_plaintext_password = false\n```\n\nFor scripts, stdin still works:\n\n```bash\nprintf '%s' \"$OBR_PASSWORD\" | ./target/release/obr hash-password\n```\n\nPlaintext passwords are disabled by default. Only enable `allow_plaintext_password = true` for throwaway local development.\n\n## Run Locally\n\nFor local development:\n\n```bash\ncargo run\n```\n\nFor the release binary:\n\n```bash\n./target/release/obr run\n```\n\nThe release binary embeds the web UI assets (`index.html`, JavaScript, CSS, service worker, manifest, and favicon). Deploying Obr does not require copying the repo `assets/` directory.\n\nCheck a deployment before opening it in a browser:\n\n```bash\n./target/release/obr doctor\n```\n\n`obr check` is an alias. The doctor command validates config, vault access,\nWebAuthn origin/RP ID settings, writable runtime data, logs, passkey storage,\nand image directories.\n\nOpen:\n\n```text\nhttp://localhost:8010/\n```\n\nFor local passkey testing, use `http://localhost:8010`, not `http://127.0.0.1:8010`, because the default WebAuthn origin uses `localhost`.\n\n## Daemon Mode\n\nRun from the repo root so relative paths in `config/local.toml` resolve correctly:\n\n```bash\n./target/release/obr daemon start\n```\n\n`obr daemon` is an alias for `obr daemon start`.\n\nLogs are written to the configured path:\n\n```toml\nlog_path = \"logs/obr.log\"\n```\n\nManage the background process with:\n\n```bash\n./target/release/obr daemon status\n./target/release/obr daemon reload\n./target/release/obr daemon stop\n```\n\nDaemon mode writes its pid file under the gitignored `data` directory.\n\n## Passkeys And HTTPS\n\nFor local testing, the default passkey settings are enough:\n\n```toml\nlisten = \"127.0.0.1:8010\"\n```\n\nFor phone or remote browser use, configure a stable HTTPS origin:\n\n```toml\nsecure_cookies = true\nwebauthn_rp_id = \"obr.example.com\"\n```\n\n`webauthn_origin` defaults to `https://\u003cwebauthn_rp_id\u003e`. Set it explicitly\nonly when the browser origin differs from that default.\n\nChanging `webauthn_rp_id` or the effective WebAuthn origin invalidates existing\npasskeys for that domain. Register a new passkey after changing the public\ndomain.\n\nOnce a passkey is registered, password login is disabled outside localhost. Localhost password login remains available as a recovery path.\n\n## Tailscale Funnel\n\nTailscale Funnel exposes a local Obr server through a public HTTPS hostname\nunder your tailnet domain, such as `\u003chostname\u003e.\u003ctailnet\u003e.ts.net`.\n\nFirst, please [install tailscale](https://tailscale.com/docs/install). The\ninit/manual flow will ask you to log in to your tailnet if this userspace\ninstance has not been authorized yet.\n\nThe `init` command can run this whole flow for you:\n\n```bash\n./target/release/obr init --vault /path/to/obsidian/vault --tailscale\n```\n\nThe examples below use a separate userspace `tailscaled` instance instead of the\nsystem Tailscale daemon. That keeps Obr's public route isolated in its own state\ndirectory and socket.\n\n```bash\nHOST=ob\nBASE=\"$HOME/.local/share/tailscale-obr\"\nSOCK=\"$BASE/tailscaled.sock\"\n```\n\nStart the separate `tailscaled` in the background:\n\n```bash\nmkdir -p \"$BASE\"\n\nnohup tailscaled \\\n  --tun=userspace-networking \\\n  --socket=\"$SOCK\" \\\n  --statedir=\"$BASE\" \\\n  \u003e \"$BASE/tailscaled.log\" 2\u003e\u00261 \u0026\n\necho $! \u003e \"$BASE/tailscaled.pid\"\n```\n\nLog this instance into your tailnet and choose the `*.ts.net` hostname:\n\n```bash\ntailscale --socket=\"$SOCK\" up --hostname=\"$HOST\" --accept-dns=false\n```\n\nIf this is the first login for this state directory, Tailscale prints an\nauthorization URL. Open it and approve the new node.\n\nWith Obr listening on `127.0.0.1:8010`, publish it through Funnel:\n\n```bash\ntailscale --socket=\"$SOCK\" funnel --yes --bg http://127.0.0.1:8010\n```\n\nCheck the public route:\n\n```bash\ntailscale --socket=\"$SOCK\" funnel status\n```\n\nThen set the WebAuthn config to the Funnel hostname:\n\n```toml\nsecure_cookies = true\nwebauthn_rp_id = \"\u003chostname\u003e.\u003ctailnet\u003e.ts.net\"\n```\n\nReplace `\u003chostname\u003e.\u003ctailnet\u003e.ts.net` with the HTTPS hostname from\n`funnel status`. For example, if `HOST=ob`, the public origin will look like\n`https://ob.\u003ctailnet\u003e.ts.net`.\n\nThe `--socket` flag is important. Without it, the `tailscale` CLI tries the\nsystem daemon socket, usually `/var/run/tailscaled.socket`, and will not talk to\nthe separate Obr `tailscaled` instance above.\n\nStop the public Funnel route without stopping `tailscaled`:\n\n```bash\ntailscale --socket=\"$SOCK\" funnel reset\n```\n\nStop the separate `tailscaled` process:\n\n```bash\nkill \"$(cat \"$BASE/tailscaled.pid\")\"\n```\n\n`tailscaled.state` is internal state maintained by `tailscaled`. Do not edit it\nby hand; change Funnel and Serve routes with the `tailscale --socket=...`\ncommands.\n\n## License\n\nObr is licensed under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenyukang%2Fobr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchenyukang%2Fobr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchenyukang%2Fobr/lists"}