{"id":50990644,"url":"https://github.com/dkackman/tauri-plugin-nostr","last_synced_at":"2026-06-20T02:03:19.517Z","repository":{"id":355309996,"uuid":"1227550725","full_name":"dkackman/tauri-plugin-nostr","owner":"dkackman","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-02T23:59:49.000Z","size":869,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-03T00:37:12.648Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dkackman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"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-02T20:57:20.000Z","updated_at":"2026-05-02T23:59:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dkackman/tauri-plugin-nostr","commit_stats":null,"previous_names":["dkackman/tauri-plugin-nostr"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dkackman/tauri-plugin-nostr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkackman%2Ftauri-plugin-nostr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkackman%2Ftauri-plugin-nostr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkackman%2Ftauri-plugin-nostr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkackman%2Ftauri-plugin-nostr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dkackman","download_url":"https://codeload.github.com/dkackman/tauri-plugin-nostr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkackman%2Ftauri-plugin-nostr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34554510,"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-06-20T02:00:06.407Z","response_time":98,"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":[],"created_at":"2026-06-20T02:03:18.682Z","updated_at":"2026-06-20T02:03:19.512Z","avatar_url":"https://github.com/dkackman.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tauri-plugin-nostr-sync\n\nEncrypted, decentralized state sync for Tauri apps using [Nostr](https://nostr.com) replaceable events as transport.\n\nThe plugin moves encrypted blobs between instances of your app via Nostr relays. Key derivation, storage, schema versioning, and conflict resolution are your app's responsibility — the plugin is transport only.\n\n[![npm](https://img.shields.io/npm/v/tauri-plugin-nostr-sync-api)](https://www.npmjs.com/package/tauri-plugin-nostr-sync-api)\n[![Crates.io Downloads (latest version)](https://img.shields.io/crates/dv/tauri-plugin-nostr-sync)](https://crates.io/crates/tauri-plugin-nostr-sync)\n\n## Status\n\nPre-1.0 and under active development. **Phase 1** (Rust state machine) and **Phase 2** (Tauri IPC commands, TypeScript bindings, configurable `Builder`) are shipped. See `specs/tauri-plugin-nostr.md` for the full design.\n\n## How it works\n\n- State is published as [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md) arbitrary custom app data (kind `30078`), which uses [NIP-33](https://github.com/nostr-protocol/nips/blob/master/33.md) parameterized replaceable events so relays automatically retain only the latest value per category.\n- Payloads are encrypted with [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) before leaving the device. Plaintext never touches a relay.\n- A signing identity is injected at runtime (e.g. after wallet unlock) via the `NostrSigner` trait — the plugin never holds raw key bytes. Publish calls before injection return `SignerNotSet`.\n- Call `poll()` periodically to check for remote updates; new results are returned and also emitted as `nostr-sync://updated` Tauri events.\n\n## Installation\n\nAdd the Rust crate to your `src-tauri/Cargo.toml`:\n\n```toml\n[dependencies]\ntauri-plugin-nostr-sync = \"0.1.0-alpha.3\"\n```\n\nAdd the JavaScript bindings:\n\n```sh\npnpm add tauri-plugin-nostr-sync-api\n# or\nnpm install tauri-plugin-nostr-sync-api\n```\n\n## Setup\n\nRegister the plugin in your Tauri app:\n\n```rust\ntauri::Builder::default()\n    .plugin(\n        tauri_plugin_nostr_sync::Builder::new()\n            .relays(vec![\n                \"wss://relay.damus.io\",\n                \"wss://relay.nostr.band\",\n                \"wss://nos.lol\",\n            ])\n            .app_namespace(\"myapp\")       // prefixes all d-tags\n            .max_payload_size(128 * 1024) // optional: default 64KB, cap 400KB\n            .build()\n    )\n    .run(tauri::generate_context!())\n    .expect(\"error while running tauri application\");\n```\n\n`tauri_plugin_nostr_sync::init()` is a convenience alias for `Builder::new().build()` with the `\"default\"` namespace and no preconfigured relays.\n\n### Payload size limit\n\nThe default maximum payload size is **64KB**. Use `.max_payload_size(bytes)` on the builder to increase it up to **400KB**. Values above 400KB are rejected at plugin startup via Tauri's setup error path. `publish` returns `Error::PayloadTooLarge` when the serialized payload exceeds the configured limit.\n\nAdd the permission to your app's capability file (`src-tauri/capabilities/default.json`):\n\n```json\n{\n  \"permissions\": [\"nostr-sync:default\"]\n}\n```\n\n## Usage\n\n### Signer injection\n\nThe signing identity is not provided at registration time — inject it after the user unlocks their keys (e.g. wallet unlock). The plugin accepts any type that implements `nostr_sdk::NostrSigner` and never stores raw key bytes internally.\n\n```rust\n// Derive a dedicated sync subkey from your wallet master key.\n// Never pass the root wallet key — use BIP-32, HKDF, or equivalent.\nlet sync_secret = derive_sync_key(\u0026wallet_master_key);\nlet signer = nostr_sdk::Keys::new(sync_secret);\n\n// After unlock\napp.nostr_sync().set_signer(signer).await?;\n\n// On lock — drops the signer and zeroes key material via ZeroizeOnDrop\napp.nostr_sync().clear_signer().await;\n```\n\n### TypeScript API\n\n```typescript\nimport {\n  publish, fetch, syncAll, poll,\n  addRelay, removeRelay, getRelays,\n  getPubkey, getStatus,\n} from 'tauri-plugin-nostr-sync-api'\n\n// Publish state for a named category (encrypted)\nawait publish('ui-settings', { theme: 'dark', fontSize: 14 })\n\n// Publish with NIP-40 expiration (Unix timestamp seconds) — relay support is the caller's responsibility\nconst oneHour = Math.floor(Date.now() / 1000) + 3600\nawait publish('ui-settings', { theme: 'dark' }, oneHour)\n\n// Fetch the latest known state for a category\nconst result = await fetch('ui-settings')\n// result: { category: string, payload: unknown, updatedAt: string, deviceId: string } | null\n\n// Fetch multiple categories at once\nconst results = await syncAll(['ui-settings', 'wallet'])\n// results: FetchResult[]\n\n// Poll for updates since last poll (deduplicates unchanged events)\nconst updates = await poll(['ui-settings', 'wallet'])\n// updates: FetchResult[] — only categories with events newer than last seen\n\n// Relay management\nawait addRelay('wss://relay.example.com')\nawait removeRelay('wss://relay.example.com')\nconst relays = await getRelays()\n// relays: Array\u003c{ url: string, connected: boolean, lastSeen: string | null }\u003e\n\n// Status\nconst status = await getStatus()\n// status: { ready: boolean, relayCount: number, connectedRelayCount: number, deviceId: string }\n\n// The sync pubkey (hex), or null if no signer is set\nconst pubkey = await getPubkey()\n```\n\n`ready` in `SyncStatus` is `true` only when a signer is set and at least one relay is connected.\n\n### Listening for remote updates\n\n`poll()` emits `nostr-sync://updated` for each new result as a side effect, so you can react to updates without inspecting the return value:\n\n```typescript\nimport { listen } from '@tauri-apps/api/event'\n\n// Fired by poll() for each category with a newer event than last seen\nawait listen('nostr-sync://updated', (event) =\u003e {\n  const { category, payload, deviceId, updatedAt } = event.payload\n})\n```\n\n\u003e `nostr-sync://relay-status` and `nostr-sync://error` are planned for a future release.\n\n### Rust API\n\n```rust\nuse tauri_plugin_nostr_sync::TauriPluginNostrSyncExt;\n\nlet sync = app.nostr_sync();\n\nsync.set_signer(signer).await?;   // impl NostrSigner + 'static\nsync.clear_signer().await;        // drops signer; ZeroizeOnDrop zeroes key material\n\nlet status = sync.status().await; // SyncStatus\nlet pubkey = sync.pubkey().await; // Option\u003cPublicKey\u003e\n\nsync.add_relay(\"wss://relay.example.com\").await?;\nsync.remove_relay(\"wss://relay.example.com\").await?;\nlet relays = sync.relays().await; // Vec\u003cRelayInfo\u003e\n\nsync.publish(\"ui-settings\", \u0026serde_json::json!({ \"theme\": \"dark\" }), None).await?;\n\n// NIP-40: optional expiration (Unix timestamp seconds); relay support is the caller's responsibility\nlet expires = nostr_sdk::Timestamp::now().as_u64() + 3600;\nsync.publish(\"ui-settings\", \u0026serde_json::json!({ \"theme\": \"dark\" }), Some(expires)).await?;\nlet result = sync.fetch(\"ui-settings\").await?;   // Option\u003cFetchResult\u003e\n\nlet categories = vec![\"ui-settings\".to_string(), \"wallet\".to_string()];\nlet all = sync.sync_all(\u0026categories).await?;     // Vec\u003cFetchResult\u003e\nlet updates = sync.poll(\u0026categories).await?;     // Vec\u003cFetchResult\u003e — only new; also emits nostr-sync://updated\n```\n\n## Design notes\n\n**Key management** — the plugin interacts with signing through the `NostrSigner` trait and never holds a raw `SecretKey`. Three rules: (1) always pass a *derived* sync keypair, not the root wallet key — use BIP-32 or HKDF to produce a dedicated identity; (2) the signer must be `ZeroizeOnDrop` so key bytes are cleared when `clear_signer` is called (`nostr_sdk::Keys` satisfies this); (3) do not downcast or clone the signer into unzeroized storage. Publish calls before `set_signer` return `SignerNotSet`.\n\n**d-tag format** — events are keyed as `{namespace}/{category}/v1`. The namespace is set via `app_namespace` at registration and prefixes all d-tags, so multiple apps can share the same keypair without collision.\n\n**Payload size limit** — payloads over 64KB are rejected immediately with a `PayloadTooLarge` error before any relay interaction. This matches the most conservative common relay cap.\n\n**Startup sync** — `syncAll()` is not called automatically. Call it explicitly after signer injection to pull the latest state from relays.\n\n**Polling vs subscribing** — `poll()` is a one-shot fetch that returns only categories with events newer than what was last seen in the current process. The deduplication state resets on restart. Call `poll()` on a timer to check for remote updates. Subscription-style push is planned for a future release.\n\n**Conflict resolution** — last-write-wins by `created_at`. Events older than or equal to the locally known latest for a category are silently discarded.\n\n## License\n\nApache License 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkackman%2Ftauri-plugin-nostr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdkackman%2Ftauri-plugin-nostr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkackman%2Ftauri-plugin-nostr/lists"}