Other Note is a private, Nostr-powered notes app for Android, Linux desktop, and the web. It stores notes as encrypted application-specific Nostr events, syncs through user-selected relays, and does not require a central Other Note server for note storage. The app is built around user-controlled identity: notes are signed and encrypted on the client device or through a user-approved signer.
- Privacy and encryption: encrypted Nostr note storage using kind
30078application-specific events; client-side or signer-delegated signing, encryption, and decryption; no plaintext note publishing; create/edit/delete support through encrypted replacement/tombstone events. - Identity and signing: Android NIP-55 signer support; web NIP-07 browser-extension support; NIP-46 remote signer/bunker support; opt-in remembered NIP-46 remote-signer sessions on web; session-only direct
nseclogin; create-new-identity flow with required key-saving warnings; Linux desktop keyring support for saved identities wheresecret-toolis available; Android Keystore-backed secure direct-key storage when explicitly enabled. - Relay sync and migration: editable note relay settings; NIP-65 relay-list import/publish behavior; relay migration when relay settings change; manual Sync/Migrate; per-relay encrypted-event stats; visible relay-retention warning recommending multiple relays and ideally a personal relay the user controls.
- Notes and rendering: note search and sort; built-in themes; profile metadata and profile thumbnail display; Markdown rendering in full-note view; hyperlinks and inline images in full-note view only, while note cards and editors keep raw text.
- Cross-platform clients: shared Android and desktop Compose UI where practical, a Debian/Linux desktop package target, and a static web client with self-hosted Roboto fonts, NIP-07/NIP-46/direct-key sign-in, encrypted note CRUD, relay tools, themes, profile display, and full-note Markdown/link/image rendering.
- Web app available at https://othernote.app.
If you like my work you can support me by donating via lightning to lprimordium@getalby.com
- Other Note cannot recover a lost private key. Save generated or pasted
nsecvalues somewhere secure if you need long-term access to that identity. - Direct
nsecsessions are session-only unless the user explicitly saves the key through a supported secure mechanism or an external password manager. - Public relays are not guaranteed to retain encrypted note events forever. Use multiple relays at minimum; ideally include at least one personal relay that you control.
- The web client does not persist notes, note events, relay events, note relay settings, profile data, search/sort state, or direct-key sessions in browser storage. The only app-controlled browser persistence is the generic theme preference and an explicit opt-in remembered NIP-46 session record.
- Remote profile images and full-note images may fetch from their source URLs when rendered.
- See docs/key-management.md, docs/web-client-architecture.md, and docs/web-deployment-security.md for detailed security boundaries.
This repository now contains a runnable project structure with shared domain, data, sync, Nostr abstraction, UI, Android entry point, desktop entry point, desktop Debian packaging configuration, a static web client, and focused tests.
Important security status:
- GPLv3 license is preserved in
LICENSE. - Private keys and decrypted note bodies are not logged.
nsecinput is only kept in memory. The UI redacts it immediately after validation.- "Create new identity" generates a fresh Nostr private key and shows the resulting
nsecso the user can save or import it. Other Note does not persist the generated plaintextnsec. - The login field hides pasted
nsectext and clears it after successful validation or local-only entry. - Local-only mode is explicit. Notes can be created, viewed, edited, and deleted without a Nostr session.
- Linux desktop saved-key support is opt-in through the desktop keyring/Secret Service path when
secret-toolis available. Android saved-key support is opt-in through Android Keystore-backed encrypted device storage. Session-only remains the default on both platforms, and there is no plaintext file fallback. ProductionNostrCryptoFactoryis enabled for direct session-only signing, NIP-44 v2 encryption/decryption, and relay-backed tests where a real relay client is wired.NonProductionNostrCryptoremains the safety fallback.NonProductionNostrCryptoremains available and refuses secp256k1 signing and NIP-44 encryption/decryption so plaintext notes are not accidentally published.- Direct
nsecand generated-identity use remains session-only unless the user explicitly saves the key to supported OS-backed storage: Android Keystore-backed encrypted storage on Android or the OS keyring on Linux desktop. These sessions can encrypt/sign/publish/fetch when production crypto and a real relay client are available. Bounded desktop/JVM and Android WebSocket relay clients surface explicit per-relay status instead of fake success. - Desktop relay runtime is enabled by default when production crypto is available. It can fetch and publish encrypted note events through configured relays for session-only direct keys and signer-backed sessions.
- Desktop relay runtime stores a local encrypted event cache and pending outbound write queue under
~/.local/share/other-note/. These files contain signed encrypted Nostr events and relay metadata only, nevernsecvalues, private keys, decrypted note bodies, decrypted payload JSON, or NIP-44 plaintext. - Android can detect generic NIP-55 external signer apps, request signer public identity, remember safe signer-session metadata, request a harmless local test-event signature, run a harmless local NIP-44 encrypt/decrypt round trip, build/verify an unpublished signer-backed kind
30078note event, and create/edit/delete/fetch relay-backed signer notes from the normal editor without importing or storingnsec. Android directnsecremains a lower-emphasis fallback: session-only by default, with an explicit Android Credential Manager password-save option and explicit Android Keystore-backed save/continue/forget actions when the user chooses them. - Android external-signer relay runtime stores a durable encrypted event cache and pending outbound write queue in app-private no-backup storage. These files contain signed encrypted Nostr events and safe relay metadata only, never
nsecvalues, private keys, decrypted note bodies, decrypted payload JSON, or NIP-44 plaintext. - Android cloud backup and device-transfer extraction are explicitly disabled for app data through the manifest and backup-rule resources. This is privacy hardening, not a backup feature.
- NIP-46 remote signer support can parse
bunker://tokens, create and save reusable NIP-46 communication sessions, request remote signer public keys, request NIP-44 encrypt/decrypt, request event signing, and validate returned signed events before relay publish. The account identity is the user pubkey returned by the remote signer, not the local NIP-46 transport pubkey. - The web target builds a static client with NIP-07 public-key sign-in, NIP-46
bunker://remote-signer public-key sign-in with explicit opt-in remembered remote-signer sessions, session-only directnsecfallback sign-in, session-only fresh identity generation, active-account profile metadata display with a small safe thumbnail, signer-backed or direct-key note create/edit/delete, full-note-only safe Markdown/link/image rendering, and session-only note relay selection held in memory only. The directnsecfield can optionally expose browser/password-manager save/fill hints when the user explicitly opts in, but that is external user-controlled password-manager behavior rather than Other Note app storage. The web client persists only the generic visual theme preference and an explicitly remembered NIP-46 communication-session record when the user opts in; it has no browser-persisted user private key, directnsec, generated identity key, note content, note events, note relay settings, relay stats, profile data, search/sort state, durable note cache, profile image cache, or persistent pending-write queue. - The web target self-hosts Roboto WOFF2 files from the static app bundle to better match Android's default
sans/Material typography. The font files are derived from Debianfonts-roboto-unhintedwith upstream source athttps://github.com/google/robotoand Apache-2.0 license attribution inweb/src/jsMain/resources/fonts/roboto/. The web client does not use Google Fonts, CDN fonts, remote font CSS, or external font hosts. - Sync is non-destructive when crypto is disabled, relay reads fail, or no relay reports a successful read.
- Payload JSON uses
kotlinx.serialization. NIP-01 event preimage serialization is kept separate from note payload serialization.
Other Note uses NIP-78 application-specific data events:
- Event kind:
30078 - Stable addressable
dtag:other-note:note:<note_id> - Tags:
["d", "other-note:note:<note_id>"]["t", "other-note"]["alt", "Encrypted Other Note note"]["client", "Other Note"]
- Event pubkey is the user's public key.
- Event content is the NIP-44 v2 encrypted note payload JSON, encrypted to self.
The encrypted note payload schema is versioned:
{
"schema": "com.libertasprimordium.othernote.note.v1",
"note_id": "<stable-note-id>",
"created_at_ms": 1234567890,
"updated_at_ms": 1234567890,
"body_markdown": "raw markdown text",
"deleted": false
}Edits publish a new signed event with the same kind/pubkey/d-tag. Relay results are grouped by d tag and note id; the newest event by created_at wins. If timestamps tie, NIP-01 replaceable/addressable behavior retains the event with the lowest event id in lexicographic order. Local state and durable encrypted-event caches may retain historical encrypted versions, but visible notes are materialized from the latest valid version and stale sync/cache results must not overwrite a newer local edit. Deletion uses an app-level tombstone with deleted=true and an empty body so relay DELETE support is not required.
Default app/note relays are editable and persist locally on Android and desktop:
wss://relay.damus.iowss://relay.primal.netwss://relay.nostr.netwss://nos.lolwss://relay.ditto.pub
Relay settings edit the app relays used for encrypted kind 30078 note fetch/publish. They do not edit NIP-46 remote-signer transport relays, which are sourced from bunker tokens and signer transport state for encrypted kind 24133 request/response traffic.
Other Note also reads and publishes NIP-65 relay-list metadata for these app relays. On login, the app looks for the user's latest public kind 10002 relay-list event on the configured discovery relays. If a published list exists and contains write/outbox relays, those write relays become the local app relay list; if no list exists or no write relays are present, local settings are retained. Local app relays are represented as write r tags. Existing read/inbox, custom, and non-r relay-list metadata from other apps is preserved where possible. NIP-46 signer-transport relays are never imported or published as app relays just because a bunker used them.
Opening Relay Settings runs a bounded one-shot refresh of the active user's latest published relay list. If published write relays changed elsewhere, they are imported into local app relay settings unless the user has unsaved relay edits, an add/test dialog, or migration in progress. Unsaved edits prompt a choice to keep local edits or reload the published list.
Users may enter relay hostnames with or without wss://; for example, relay.primal.net is normalized to wss://relay.primal.net. Production relays should use wss://; ws:// is accepted only for local development hosts such as localhost. Query strings, fragments, malformed URLs, duplicate normalized URLs, and http:// or https:// URLs are rejected. The settings screen can restore the default relay set, and saving an empty relay list is blocked because relay sync and publishing require at least one app relay.
New relays are tested before being added. Direct/session-only key sessions publish and fetch a harmless non-note test event from the candidate relay. Signer-backed or local-only contexts use a bounded read/connect test instead, so relay testing does not mutate NIP-46 signer-transport relays or issue raw signer request payloads. If the test succeeds, the relay is added silently. If it fails, Other Note shows a safe warning and asks whether to cancel or continue adding the relay anyway.
Public relays are not guaranteed to retain encrypted note events forever. Use multiple relays at minimum. Ideally, include at least one personal relay that you control.
Relay changes execute a local migration plan before settings are finalized. Other Note first signs and publishes an updated public kind 10002 relay-list event to the union of old and requested app relays, then fetches signed encrypted note history from the current relays before removal, keeps the encrypted event cache, selects the latest signed encrypted event for each note address, and republishes those already-signed encrypted events to newly added relays. If relay-list publishing, removed-relay fetch, or added-relay writes partially fail, the settings screen shows a safe warning and lets the user cancel or continue. Migration never decrypts and republishes plaintext, and it does not touch NIP-46 signer-transport relays.
Relay Settings also has a manual Sync/Migrate action for the current app relay list. Use it after importing a relay-list change from another app: it fetches encrypted note events from the current app relays, merges them into the encrypted cache, selects the latest signed encrypted event per note address including tombstones, and republishes those existing signed encrypted events to the current app relays without requiring a local add/remove edit.
Current direct nsec use is session-only by default. On Android, the user may explicitly request an Android Credential Manager password-save flow for the direct field, or explicitly save an identity to Android Keystore-backed encrypted device storage. On Linux desktop, the user may explicitly save an identity to the desktop keyring/Secret Service path when available. Other Note does not persist nsec values or private keys in plaintext app files, relay settings, caches, pending-write stores, logs, or preferences. Android secure storage and desktop keyring storage are device-local convenience storage, not backups.
The sign-in screen can create a fresh Nostr identity. This generates a new private key, displays the nsec for the user to save in a secure password manager, OS credential store, or signer such as Amber, and requires explicit acknowledgement before the key is used for the current session. On Android and Linux desktop, the generated identity can also be saved explicitly to supported OS-backed secure storage. When production crypto and a relay client are available, the generated session can encrypt, sign, publish, decrypt, edit, and delete encrypted kind: 30078 notes. Losing the generated nsec means losing access to encrypted notes for that identity if no signer/keyring/password-manager copy exists. Other Note does not silently store or recover it.
The key-management policy is documented in docs/key-management.md. The sign-in screen prioritizes Android signer/NIP-55 first on Android, then NIP-46 remote signer/bunker when available, then session-only pasted nsec, with fresh identity generation as a lower-emphasis deliberate flow. Persistent direct-key convenience storage is explicit and OS-backed on Android and Linux desktop. Other Note Web keeps signing, encryption, and decryption fully client-side or signer-delegated; see docs/web-client-architecture.md.
Android builds include NIP-55 discovery, public-key request, internal coverage for signer primitives, and relay-backed signer note creation/edit/delete/recovery:
- The manifest declares a
nostrsigner:query so the app can discover compatible Android signer apps. - Discovery is generic NIP-55 intent discovery, not Amber-only. Amber is the primary tested signer target, but any compatible signer can be detected.
- When a NIP-55 signer is detected, the login screen presents Android signer as the recommended Android path, shows remote signer/bunker as an advanced secondary option when available, and keeps the direct
nsecfield as a lower-emphasis session-only fallback. - Pressing "Use Android signer" explicitly launches a NIP-55
get_public_keyintent. If the signer approves, Other Note creates a signer-backed session with public key,npub, and signer package metadata only. - After successful Android signer login, Other Note can remember the signer package, account pubkey, and local active-session state in app-private no-backup storage. Subsequent launches can restore the authenticated app shell without opening Amber just to continue the session.
- Remembered Android signer metadata is not saved-key storage: the user private key remains in the signer app. Log out disables automatic session restoration, and "Forget Android signer" removes only this app's saved signer metadata from the device.
- Internal tests and helper code cover the NIP-55
ContentResolverSIGN_EVENTpath using harmless unpublished events. These development test actions are no longer exposed in the normal app UI. - The initial
get_public_keyrequest asks for optionalsign_eventpermissions for kind1, encrypted note kind30078, and relay-list kind10002, plus NIP-44 encrypt/decrypt permissions so compatible signers can approve the ContentResolver requests. - Signer-built note events are validated locally: pubkey must match the signer-backed session, content/kind/tags must match the request, and the NIP-01 event id/signature must verify.
- Signer NIP-44 encrypt/decrypt operations are used by the real note save/edit/delete/recovery workflow. The app does not display plaintext payloads, ciphertext, signatures, full event JSON, or full public keys.
- Normal editor save/edit in an Android signer session builds a signed encrypted kind
30078event through signer NIP-44 plusSIGN_EVENT, validates/decrypts/decodes it, publishes it to the configured relays, and displays the note after at least one relay accepts the write. Delete creates a signer-backed tombstone event with the same d tag, publishes it, and hides the note after at least one relay accepts. - Android signer login fetches kind
30078Other Note events for the signer pubkey from the configured relays, validates id/signature before decrypting, decrypts through signer NIP-44, decodesNotePayload, reduces replacements/tombstones, and updates visible notes incrementally as relays return usable events. - Android signer relay cache and pending writes are durable in app-private no-backup storage. The cache stores signed encrypted kind
30078events for the signer pubkey so notes can be recovered after relaunch before or alongside relay fetch. The pending-write queue stores already signed encrypted events, target relays, relay statuses, retry counts, safe truncated relay errors, and timestamps so unfinished fanout can retry on the next signer login or sync. - Safe NIP-55 diagnostics can be enabled with
OTHER_NOTE_SHOW_NIP55_DIAGNOSTICS=1or-Dothernote.showNip55Diagnostics=true. Diagnostics include only path, shape, lengths, booleans, abbreviated ids/pubkeys, result status, and result column/key names. - No
nsecor private key is stored, logged, or sent to a relay/server as part of signer discovery.
Manual Android signer test with Amber or another NIP-55 signer:
- Install a NIP-55-compatible Android signer such as Amber.
- Install/run the debug Android build.
- Tap "Use Android signer."
- Approve the public-key request in the signer.
- Confirm Other Note shows the abbreviated
npuband starts a relay sync. - Force close or relaunch before logging out and confirm Other Note restores the signed-in app shell without opening Amber for login approval.
- Confirm the old development-only signer test buttons are not visible in the normal notes UI.
- Create a normal note through the editor and tap "Save."
- Approve required signer encrypt/sign/decrypt requests.
- Confirm at least one relay accepts the write and the note appears.
- Force stop or relaunch the app, confirm the saved Android signer session restores, and confirm the cached note can appear before or alongside relay fetch.
- Confirm the note is fetched, decrypted, and displayed after relay sync completes.
- If practical, simulate partial relay fanout by including one unreachable relay, relaunch, log in with Android signer, and confirm the pending write retries on the next sync/login.
- Inspect app-private durable store files under the debug app data directory and confirm no
nsec, private key, decrypted note body, or decrypted payload JSON such asbody_markdownappears. - Edit the note, approve signer requests, relaunch, and confirm the replacement appears.
- Open the note, tap "Delete", and confirm.
- Approve required signer encrypt/sign/decrypt requests and confirm the note disappears after at least one relay accepts the tombstone.
- Relaunch and confirm the tombstoned note stays hidden.
- Log out, force close, and relaunch; confirm automatic Android signer restoration is disabled after logout.
- Use "Continue with Android signer" and confirm it restores locally without the full signer-selection flow.
- Use "Forget Android signer" and confirm only the saved signer option disappears; fresh signer selection still works.
- If signer operations fail, enable NIP-55 diagnostics and capture only the safe request path, payload length, field booleans, result status, and result columns.
- If relay recovery fails, enable relay diagnostics and capture only safe per-relay status, counts, and rejection classes.
- Confirm the direct
nsecfallback remains hidden/password-style and session-only.
Other Note includes production-safe NIP-46 remote signer / bunker login:
- The login screen accepts a
bunker://<remote-signer-pubkey>?relay=<relay>&secret=<optional-secret>token. - The login screen explains that the private key stays in the remote signer, summarizes the permissions Other Note requests, and shows pairing progress while waiting for signer approval.
- The app creates a fresh local NIP-46 communication keypair for new pairing. This keypair is distinct from the user account key and can be saved as a remote-signer session after successful pairing. On web, saving this session is explicit opt-in through "Remember this remote signer on this browser."
- After a successful pairing, Other Note can save the reusable NIP-46 communication session so the same bunker can be used after logout or restart without another one-time
connecthandshake. This saved session is not the user's private key, but it is sensitive because it can request approved signer actions; use "Forget remote signer" to remove it from this device/browser. - NIP-46 request and response events use kind
24133and are encrypted through NIP-44 between the session communication key and the remote signer pubkey. - Direct
bunker://connect requests send the remote signer pubkey from the token as the firstconnectparam, followed by the optional token secret and requested Other Note permissions. The temporary communication pubkey is only the request event author and transport encryption key. - After connect, Other Note calls
get_public_keyand uses that returned user pubkey as the note account identity. - NIP-46 signer-transport relays are separate from the app's encrypted note relays. Signer relays carry kind
24133app/signer requests and responses; note relays carry signed encrypted kind30078note events. - A bunker or signer may return or internally use its own transport relays. Those relays may differ from Other Note's editable note relay list.
- Relay order from bunker tokens, signer metadata, or
switch_relaysis treated as advisory. Other Note keeps all valid signer relays as candidates, records session-local relay behavior, and prefers relays that actually accepted the temporary communication pubkey and returned a matching response. - Write-restricted signer relays can reject the temporary NIP-46 communication pubkey even when the final user account pubkey is allowed. Other Note treats that as a remote signer transport failure, not as an encrypted note publish failure.
- Remote
sign_eventresponses are validated for expected kind, content, tags, timestamp, pubkey, id, and signature before any relay publish. - Remote
nip44_encryptandnip44_decryptare used for note save/edit/delete/recovery. The remote signer may see plaintext note payloads during these operations by design; use only a remote signer you trust with note plaintext. - Durable event cache and pending-write stores remain unchanged: they persist only signed encrypted events and safe relay metadata scoped by account pubkey. NIP-46 token secrets, decrypted notes, and decrypted payload JSON are not persisted. The reusable NIP-46 client communication key may be stored separately as a saved remote-signer session so the bunker can recognize this app after restart.
- Pending relay writes retry by republishing already signed encrypted events for the same user pubkey; retry does not require storing a user private key.
- Client-initiated
nostrconnect://URI generation is available in the protocol layer for tests; the current new-pairing login path is pastedbunker://. - Android NIP-46 login and remote signer requests run from background coroutines. Relay publish/fetch waits, response polling, JSON parsing, and NIP-44/signing work must not block Compose input dispatch or recomposition.
- Editor save keeps the draft open while remote signer requests are pending. Signer-transport failures are shown inside the editor and do not create a local note or enqueue an app note pending write.
Manual NIP-46 bunker checklist:
- Start or choose a NIP-46-compatible bunker/remote signer.
- Copy a
bunker://token with at least onewss://relay. - Paste the token into "Paste bunker:// remote signer token" and connect.
- Confirm Other Note displays the connected user
npub. - Logout or restart, then use "Continue with remote signer" and confirm it resumes without a fresh bunker token.
- Save a note and approve the remote signer flow where applicable.
- Confirm the published event is encrypted kind
30078. - Relaunch the app, resume the saved remote signer, and confirm cached encrypted events and relay fetch can restore notes.
- Edit the note and confirm replacement sync.
- Delete the note and confirm the tombstone hides it after relay acceptance.
- Inspect durable store files and confirm they contain no
nsec, private key, bunker secret, plaintext note body, or decrypted payload JSON such asbody_markdown. - Reject or cancel a remote signer request and confirm the app shows a clean error without changing local notes.
- Stop the remote signer or relay and confirm timeout/failure messages do not expose token secrets, plaintext, or raw signer responses.
- Test partial relay failure and confirm local visible notes are preserved.
- If the signer uses a write-restricted kind
24133transport relay, confirm the editor stays open and shows a remote signer relay rejection rather than hanging or navigating away.
NIP-46 bunker troubleshooting:
- If connection fails during the first request, check whether at least one configured relay accepted a kind
24133write from Other Note's temporary NIP-46 communication pubkey. - Whitelist or write-restricted relays may reject the temporary communication pubkey even when the eventual user account pubkey is allowed.
- This is distinct from encrypted note publish failure. A signer-transport failure affects kind
24133app/signer requests; note-publish failure affects kind30078encrypted note events sent to the app's note relays. - For first compatibility testing, use a public relay that accepts writes from new pubkeys and is also watched by the bunker. If Amber or another bunker injects a write-restricted personal relay, Other Note reports a signer-transport failure rather than waiting for a response that cannot arrive.
- Relays may return stale kind
24133responses for the same temporary communication pubkey. Other Note ignores decrypted responses whoseiddoes not match the current request and times out with safe candidate/decrypt/mismatch counts if no matching response arrives. - Remote signer timeouts include the safe NIP-46 method name, short request ID prefix, relay source, attempted relay count, per-relay subscribed booleans, per-relay publish status, response candidate count, decrypt failure count, mismatched ID count, matched-response boolean, and elapsed time. During save this distinguishes encryption, signing, and decryption requests.
- Treat
bunker://secrets as sensitive and one-time-use. Regenerate the token after sharing screenshots, logs, or relay diagnostics. - If the signer says the app is already connected but Other Note has no saved remote signer session, forget/revoke the old app connection in the signer and pair again.
- Other Note uses the remote-signer-returned user pubkey as the account identity. The NIP-46 communication pubkey is not the note account.
- NIP-46 request/response handling uses a dedicated live relay path: for each signer transport relay, Other Note opens the kind
24133response subscription before publishing the encrypted request to that same relay, races relays, and returns the first decrypted response whose request ID matches. It no longer treats remote signer requests as normal note fanout plus post-publish history fetch. - Signer relay selection is behavior-based, not order-based. If one candidate rejects the temporary communication pubkey, only that relay is marked failed and the request continues on other candidates.
switch_relaysoutput remains advisory until a returned relay proves it can accept the request and return the matching response. - If Amber approval appears but Other Note later times out, this is still a response-path issue; the app should remain responsive for the full timeout window and must not show an Android ANR.
Prerequisites:
- JDK 17 or newer.
- Android SDK for Android builds.
- Node.js and npm for the Kotlin/JS web build. The Gradle configuration uses the system Node executable instead of downloading Node or Yarn.
- Network access the first time Gradle resolves Compose Multiplatform and Kotlin plugin artifacts.
local.propertiesis required for Android builds whenANDROID_HOMEis not set. It should containsdk.dir=/path/to/android/sdkand must not be committed.- Debian packaging requires a full JDK 17+ with
jpackage. Android Studio's bundled JBR is not enough. - Debian/Linux desktop link opening uses Java Desktop browse when available and falls back to
xdg-open; normal desktop environments should providexdg-open.
Commands:
./gradlew :app:run
./gradlew :app:desktopTest
./gradlew :app:desktopJar
./gradlew :app:assembleDebug
./gradlew :app:packageDeb
./gradlew :app:check
./gradlew :web:jsBrowserDistributionDesktop relay runtime is enabled by default when production crypto is available. The legacy developer flag is still accepted for compatibility, but it is no longer required:
OTHER_NOTE_ENABLE_DEV_RELAY_RUNTIME=1 ./gradlew :app:runThe equivalent JVM property is -Dothernote.devRelayRuntime=true. A pasted nsec is used only for the active process session unless the user explicitly saves it to supported OS-backed storage. Saved Android identities are loaded from Android Keystore-backed encrypted storage, and saved desktop identities are loaded from the keyring by account pubkey. Both are shown on the sign-in screen using safe metadata only. Other Note does not write plaintext keys to local files, relay settings, cache, logs, preferences, or test output.
Desktop relay runtime keeps a local durability layer at ~/.local/share/other-note/:
event-cache/stores signed encrypted kind30078events keyed by account pubkey.pending-writes/stores already signed encrypted events plus target relay URLs, accepted/failed relay status, retry counts, and safe error strings.relay-settings.jsonstores only the local app relay URL list.- Pending writes are retried on login, sync, or manual refresh for the same account pubkey. The retry path republishes the existing signed event and does not require storing or re-signing with an old private key.
- The retry policy is intentionally bounded for now: unfinished relays are retried up to three times per stored pending write. A pending write is removed after every target relay accepts it or all unfinished target relays hit the retry cap.
- Deleting this local directory clears the cache and pending retry queue, but it does not delete events already published to relays.
Android external-signer relay runtime keeps the same durable encrypted event and pending-write record shape in app-private no-backup storage:
event-cache/stores signed encrypted kind30078events keyed by account pubkey.pending-writes/stores already signed encrypted events plus target relay URLs, accepted/failed relay status, retry counts, safe error strings, and timestamps.relay-settings.jsonstores only the local app relay URL list.- The files are used only after Android signer login for the same public key. Loading the encrypted event cache does not require network or signer access, but decrypting cached events still requires signer-mediated NIP-44 decrypt for the active session.
- Direct
nsecfallback remains session-only unless explicitly saved to Android Keystore-backed encrypted storage or the Linux desktop keyring. When production crypto and a relay client are available it can publish/recover encrypted notes; otherwise it remains local/offline and must not emit plaintext.
Normal UI shows compact relay status only. To show verbose safe relay diagnostics while debugging, launch with:
OTHER_NOTE_SHOW_RELAY_DIAGNOSTICS=1 ./gradlew :app:runThe equivalent diagnostics JVM property is -Dothernote.showRelayDiagnostics=true.
Desktop relay runtime defaults:
wss://relay.damus.iowss://relay.primal.netwss://relay.nostr.netwss://nos.lolwss://relay.ditto.pub
Manual relay checklist:
- Create or obtain a throwaway
nsec. - Launch with
./gradlew :app:run. - Log in with the throwaway
nsecand confirm the abbreviatednpubappears. - Create a note and verify at least one relay accepts the write.
- Restart the desktop app, log in with the same throwaway
nsec, and refresh/fetch the note. - Edit the note and verify refresh keeps only the newest version.
- Delete the note and verify refresh/restart hides the tombstoned note.
Manual generated-identity checklist:
- Open the sign-in screen.
- Tap "Create new identity."
- Confirm the warning screen explains that the generated
nsecis a private key and cannot be recovered by Other Note. - Generate the identity and confirm an
npubappears. - Reveal the
nseconly when ready to save it. - Confirm "Use for this session" stays disabled until both acknowledgements are checked.
- Save/import the
nsecoutside Other Note, then use it for the session. - Create, edit, and delete a note. Confirm no "NIP-44 v2 encryption is not wired yet" warning appears when production crypto is available.
- Logout or force-stop/relaunch and confirm the generated key is not remembered automatically.
- Re-enter the saved
nsecthrough the direct session-only path or import it into a signer, then sync and confirm encrypted notes can be recovered/decrypted. - Inspect app-private durable stores and confirm the exact generated
nsec, raw private key, plaintext note body,body_markdown, and token secrets are absent. - Confirm Android NIP-55 and NIP-46 login paths still appear and work as before.
Runtime troubleshooting:
- Save/edit/delete is best-effort. A note becomes locally visible after the local encrypt/sign/validate/decrypt control passes and at least one relay accepts the write.
- Remaining relay writes continue in the background after the first accepted write. Compact status reports aggregate progress; verbose per-relay status is hidden unless diagnostics are enabled.
- If the app exits after the first accepted relay but before fanout completes, unfinished relay writes remain queued locally and are retried on the next login/sync for the same pubkey.
- Cached encrypted events are loaded after login and decrypted only with the session key supplied for the current process, so notes can appear before network fetches complete.
- Slow relays may fail, reject, or time out. One slow relay should not block a successful write from a faster relay.
- Sync applies valid events incrementally as each relay completes. A recovered note can appear before the final aggregate sync status arrives, and later relay results can still update newer replacements or tombstones.
- Verbose fetch diagnostics include safe timing fields such as
duration_ms, query shape, fetched event counts, valid event counts, and rejected reason classes. They do not include keys, ciphertext, plaintext, note bodies, or decrypted JSON. - If no note appears after sync, enable relay diagnostics to distinguish: no relay returned events, returned events were rejected, all relays failed/timed out, or the newest event is a tombstone.
- Partial relay failures are expected on public relays. Retry refresh or remove consistently slow relays from the editable relay list.
- Current desktop and Android relay recovery uses direct NIP-01 filtered fetch: first author/kind/
#t, then author/kind fallback with local Other Note filtering. NIP-77/negentropy is outside the current release scope; it learns event IDs and still requiresEVENT/REQtransfer for event bodies.
Non-Linux desktop keyring backends, richer client-initiated NIP-46 pairing UI, and remote profile banner rendering are outside the current release scope. Full-note view can render the tested Markdown subset, safe links, and supported HTTPS inline note images; the signed-in identity line can render a safe profile thumbnail; note cards and editors keep Markdown, URLs, and image references as raw text.
If Gradle reports missing plugin artifacts, run with network access so it can fetch GPL-compatible open-source dependencies from Google Maven, Maven Central, and the Gradle Plugin Portal.
The web output is generated under web/build/dist/js/productionExecutable/. It supports in-memory NIP-07 public-key sign-in, NIP-46 bunker:// remote-signer public-key sign-in with default session-only behavior and explicit opt-in remembered remote-signer reconnect, session-only direct nsec fallback sign-in with default-off browser/password-manager form hints, session-only fresh identity generation, encrypted note loading, signer-backed or direct-key note create/edit/delete, local in-memory note search and sort, selectable built-in visual themes, full-note-only safe Markdown/link/image rendering, active-account profile metadata display with a safe thumbnail, session-only note relay selection with per-relay encrypted-event stats, session-only import of the active user's published kind 10002 write relays, bounded session-only relay migration for encrypted note events when note relays change, and manual session-only Sync/Migrate for the current web note relay list. The web client may persist only the generic theme ID under on.web.theme and the explicit remembered NIP-46 communication-session record under on.web.nip46; it has no durable direct-key sessions, durable generated identities, durable note caches, persistent pending writes, durable note relay preferences, durable note search/sort preferences, or durable web relay migration queues. Web static hosting and CSP checks are documented in docs/web-deployment-security.md.
Normal local validation:
git diff --check
./gradlew :app:desktopTest
./gradlew :app:desktopJarAndroid validation when the Android SDK is configured:
./gradlew :app:check
./gradlew :app:assembleDebugDesktop package validation when the active JDK includes jpackage:
./gradlew :app:packageDebFor install/runtime artifact checks before treating APK or Debian package output as a test candidate, see docs/release-smoke-test.md.
Relay integration tests are intentionally opt-in. They generate throwaway keys at runtime, publish encrypted disposable kind 30078 test blobs, fetch them back, publish an update, and publish an app-level tombstone. Public relays may retain those encrypted blobs.
OTHER_NOTE_RELAY_TESTS=1 OTHER_NOTE_TEST_RELAYS=wss://relay.example.com ./gradlew :app:desktopTestPublic relay behavior varies; the integration test requires at least one configured relay to accept each write and reports per-relay read/write status when assertions fail.
The production crypto boundary is NostrCrypto plus ProductionNostrCryptoFactory.
The offline production adapter is backed by Quartz 1.11.0 for:
- Throwaway private-key generation.
- NIP-19
nsec/npubencode/decode. - Public-key derivation from private key.
- NIP-01 canonical event preimage, id hashing, Schnorr signing, and signature validation.
- NIP-44 v2 encryption/decryption to self.
Quartz is MIT-licensed and therefore GPLv3-compatible. The previous Quartz 1.03.0 adapter was disabled after focused desktop/JVM tests reproduced a local NIP-44 v2 Invalid Mac failure when decrypting ciphertext created with the same generated keypair. The current spike upgrades to Kotlin 2.3.21, Compose Multiplatform 1.11.0, Android Gradle Plugin 8.13.2, Gradle 8.13, Quartz 1.11.0, kotlinx.coroutines 1.11.0, kotlinx.serialization JSON 1.11.0, and Android compile SDK 37.
The real crypto round-trip tests live under desktopTest. They generate throwaway keypairs, perform repeated NIP-44 self-encryption round trips, sign and validate kind 30078 events, decrypt event content, and cover tombstone payloads before relay integration tests are run.
DesktopNostrClient and AndroidNostrClient implement bounded NIP-01 WebSocket publish and fetch behavior for their runtime targets:
- Sends
["EVENT", event]and waits for matchingOK. - Sends
REQfilters for author, kind30078,#t=["other-note"], and a bounded limit. - Reads
EVENT,EOSE,CLOSED,NOTICE, andOKrelay messages. - Sends
CLOSEafter bounded fetches. - Falls back to author/kind filtering if tag filtering does not return usable events.
The desktop client is wired into the normal Debian/desktop runtime when production crypto is available. The legacy OTHER_NOTE_ENABLE_DEV_RELAY_RUNTIME=1 and -Dothernote.devRelayRuntime=true switches are still accepted as compatibility markers but are no longer required for relay fetch/publish. The Android client remains wired for Android external-signer, NIP-46, and production-crypto direct identities. Direct nsec fallback remains session-only unless explicitly saved to Android Keystore-backed encrypted storage or the Linux desktop keyring; it falls back to local/offline only if production crypto is unavailable.
Shared commonMain code is organized into:
domain: notes, relay config, session, sync state.nostr: NIP-19 decoding, NIP-44 status, event model, crypto/client/repository interfaces.data: note store, relay settings, profile cache, secure key-store abstraction, encrypted event cache, and pending write queue interfaces.sync: save, delete/tombstone, sync reduction, and local relay migration execution.ui: shared Compose screens for login, list, display, edit, and relay settings.util: URL detection, npub detection, markdown helpers, relay URL validation, payload JSON codec.
Platform code:
androidMain: Android activity, NIP-55 signer adapters, bounded Android relay client, and app-private durable encrypted-event cache/pending-write stores for external-signer sessions.desktopMain: Compose Desktop window entry point, bounded WebSocket relay client, and file-backed developer runtime cache/pending-write stores.web: standalone Kotlin/JS static web client. It intentionally does not depend on the native app runtime. Current web auth includes in-memory NIP-07, default session-only NIP-46 public-key sign-in with explicit opt-in remembered remote-signer reconnect, session-only directnsec, and session-only generated identities, with in-memory note loading, local note search/sort, persisted generic theme preference, signer-backed create/edit/delete, profile metadata, session-only note relay selection and per-relay encrypted-event stats, session-only kind10002write-relay import, best-effort relay-list publishing, bounded session-only encrypted note relay migration on relay changes, and manual session-only Sync/Migrate for current web note relays when the active signer supports the required capabilities. Browser storage for user private keys, directnsec, generated identities, notes, note events, note relay settings, relay stats, profile data, durable relay preferences, durable web relay migration queues, durable note caches, durable note search/sort preferences, and persistent pending writes is absent.
- Keep the production crypto adapter covered by offline generated-key tests before expanding runtime relay sync.
- Broaden desktop keyring support beyond Linux Secret Service where needed.
- Keep remote profile image loading limited to the active-account thumbnail; profile banners and profile image caching remain disabled.
- Keep the practical Markdown renderer focused on the tested subset; tables, task lists, raw HTML rendering, math, diagrams, syntax highlighting, video, and richer embeds are outside the current release scope.
Other Note is licensed under the GNU General Public License version 3. See LICENSE.