Dart NDK
Dart NDK gives Flutter and Dart developers a maintained Nostr toolkit instead of asking every app to rebuild relay selection, signer handling, cache strategy, wallet flows and protocol usecases from scratch.
A Dart toolkit, not another client
Dart NDK is a developer library for people building Nostr software in Dart and Flutter. That first sentence matters because the name can mislead in three directions. It is not Android NDK, it is not the TypeScript package from the `nostr-dev-kit` organization, and it is not a user-facing Nostr app you install to read a timeline.
The public repository is `relaystr/ndk`, and its README calls it Dart Nostr Development Kit. The project describes itself as a Dart library that improves the Nostr development experience, giving builders high-level usecases such as lists and metadata while still leaving room for low-level relay queries.
That is the useful mental model. Dart NDK sits below the app experience. A Flutter client, live-streaming app, wallet-adjacent tool or publishing product can use it to handle repeated protocol work: connect to relays, verify events, sign or request signatures, cache data, find user relays, publish events, use NWC, inspect zaps and fetch files.
The library belongs under Developer Tools & Libraries because its readers are builders. A normal user may benefit from an app built with Dart NDK, but the page is not evaluating a finished social feed. It is evaluating the toolkit that makes a Nostr app less likely to rebuild the same infrastructure badly.
Why Flutter builders need this layer
Flutter is attractive for Nostr because it can reach Android, iOS, desktop and web from one codebase. The hard part is that Nostr is not one server API. A client has to choose relays, parse event kinds, handle disconnected websocket state, verify signatures, protect keys, cache data, deal with signer prompts and keep product screens responsive while events arrive.
Dart NDK tries to package those recurring chores into reusable usecases. The docs emphasize constrained clients, especially mobile devices. That focus is important. A phone has battery limits, intermittent network, OS lifecycle rules and a user who notices immediately when signature verification or a relay storm makes scrolling feel heavy.
The library also gives Flutter builders a more direct route into newer Nostr patterns. Its public feature list includes NIP-47 Nostr Wallet Connect, NIP-57 zaps, NIP-59 gift wrap, NIP-42 relay authentication, NIP-51 lists, NIP-05 domain verification, Blossom file handling, proof of work and NIP-77 set reconciliation. Those are not small side quests for an app team.
A toolkit does not remove product judgment. It gives a safer starting point. The app still decides which usecases belong in the interface, how much wallet permission to request, what data to persist, what failure language to show and whether a feature is mature enough for real users.
The public project state
GitHub metadata checked on June 14, 2026 identifies the repository as `relaystr/ndk`, created on October 7, 2023, pushed on June 13, 2026, written primarily in Dart and licensed under MIT. The same metadata showed 27 stars, 8 forks and 66 open issues.
Those numbers are useful context, not a verdict. The project is active and has a real docs site, but it is still small enough that a production team should read code and issues rather than treating it as invisible infrastructure. A mobile client can inherit both a library's strengths and its unsettled edges.
The homepage points to `dart-nostr.com`, which lists apps using NDK, including the sample app, Yana, Camelus, ZapStream, Zapstore, Freeflow, Hostr, Bitblik, Donow and Submarine. That matters because library design becomes more credible when multiple downstream apps push against real usecases.
OpenSats also covered Dart NDK in its June 1, 2026 report on Nostr libraries, describing it as part of the broader NDK family for Flutter and Dart developers. That external writeup is not a substitute for the repo, but it confirms the project is recognized as shared ecosystem plumbing, not only a private experiment.
Stable package versus dev line
The version picture needs careful reading. Pub metadata for `ndk` showed 0.8.3 as the latest stable release, published on May 15, 2026. The same Pub API showed prereleases through 0.8.4-dev.4, published on June 13, 2026. The repository `packages/ndk/pubspec.yaml` also declared 0.8.4-dev.4.
The changelog explains why that distinction matters. The 0.8.4 dev line includes fixes around Cashu preflight checks, missing timeouts, typed NIP-05 errors, Blossom upload hashing and signer request concurrency. Some of those are exactly the kind of things app teams want, but a dev version can still carry API churn.
The safe product habit is to pin the exact package version and write down why. If you choose 0.8.3, you are choosing the stable line. If you choose 0.8.4-dev.4, you should be able to point to a specific fix and accept the cost of testing a prerelease surface across every platform you ship.
Do not mix docs, changelog and Pub state casually. A docs page can say `v0.8.1` in its chrome, Pub can report 0.8.3 stable, and GitHub can be ahead on dev packages. The integration decision belongs to the version you actually import.
The monorepo is split on purpose
The root README says NDK ships in small packages so Dart and Flutter projects can choose dependencies more granularly. The core package lives under `packages/ndk`. The monorepo also includes `nip07_event_signer`, `objectbox`, `ndk_flutter` and a sample app.
That split is practical. A non-visual Dart service should not need Flutter widgets. A Flutter client may want the UI helpers from `ndk_flutter`. A browser-targeted app may need NIP-07 signer support. A mobile app that wants persistent local storage may add ObjectBox. A project can take the core and then add adapters deliberately.
The stable Pub metadata checked here showed `ndk_flutter` 0.8.3, `nip07_event_signer` 1.0.10, `ndk_objectbox` 0.2.12 and `ndk_rust_verifier` 0.5.0. Their repo package files, like the core package, were already on dev versions in master.
This package split is also a boundary marker for readers. Dart NDK is not just a single helper function. It is a toolkit family. When you evaluate a downstream app, ask which packages it uses, whether it relies on stable or dev versions, and whether it has tested those dependencies on its actual target platforms.
The Ndk object exposes usecases
The `Ndk` class is the main entry point. Its source exposes low-level requests and broadcasts, then higher-level usecases for accounts, bunkers, follows, metadata, user relay lists, lists, relay sets, NIP-05, files, Blossom, gift wrap, connectivity, proof of work, NWC, zaps, search, Cashu, wallets, fetched ranges, NIP-77 and trusted assertions.
That design tells you how the project wants to be used. A builder can query relays directly with filters when necessary, but for common behavior the library offers named usecases. This reduces the temptation to scatter raw websocket and event-kind logic across a Flutter codebase.
The configuration object makes the same point. `NdkConfig` requires an event verifier and cache manager, accepts an event signer factory, chooses an engine mode, sets bootstrap relays, manages default timeouts, controls fetched range tracking, supports lazy or eager NIP-42 auth and can hold a Cashu seed phrase.
Those knobs are powerful. They are also where mistakes gather. A production app needs to know which verifier is used, where cache data lives, whether auth reveals identity early, which relays are bootstrapped and whether experimental wallet features are enabled. The library gives the knobs; the app owns the choices.
Relay selection is a first-class concern
Nostr clients often fail quietly when they treat relays as a single global list. The Dart NDK docs put inbox and outbox gossip near the center of the story. The homepage says automatic relay discovery uses the inbox/outbox model for better relay selection, and the performance page calls that model the main pillar for avoiding unnecessary Nostr requests.
The gossip guide says the simplest way to enable inbox/outbox behavior is the `JIT` engine, because it does the work automatically. Builders who need more granular control can use the `RELAY_SETS` engine and pass relay sets into usecases.
The guide also shows a subtle operational truth: if the cache lacks NIP-65 data for a user, broadcasting can fall back to default bootstrap relays. The suggested fix is to populate the cache with the UserRelayLists usecase, which orients itself around NIP-65 and can use NIP-02 contact-list relay data when NIP-65 is unavailable.
For a reader, the risk is stale or incomplete relay knowledge. An app built with Dart NDK can make smarter relay choices, but it still needs to test new accounts, users without NIP-65 data, users with outdated relay lists, relay outages, private relays, relay-auth challenges and the moment when a cached route stops matching reality.
Caching is part of the product
The persistence guide lists several cache options: `MemCacheManager`, `DbObjectBox`, `SembastCacheManager` and `DriftCacheManager`. It also says a custom database can implement the `CacheManager` interface. That is a real product decision, not a small backend detail.
An in-memory cache is useful for tests, examples and small experiments. A mobile app with a timeline, contacts, metadata, relays, file references and wallet-adjacent state needs a persistence strategy that survives app restarts. ObjectBox, Sembast and Drift have different operational and platform tradeoffs.
The docs recommend using the database only for NDK and running a secondary database for the app's own data. That is good advice because Nostr events and app-native entities are not the same thing. The app may need custom indexes, moderation state, drafts, local user preferences and product records that should not be confused with raw event cache.
Caching also creates privacy and correctness questions. Relay results can be stale. Deleted or replaced events can remain visible if cache invalidation is wrong. Metadata and contact lists can expose a user's social graph on disk. Developers should decide what is stored, how it is encrypted if needed, when it is pruned and how users can reset it.
Signer abstraction is useful and risky
The signers documentation defines signers as the components responsible for signing events and encrypting or decrypting messages. Dart NDK supports a local BIP-340 signer, NIP-46 remote signing, NIP-07 browser extension signing and Amber on Android, all behind a unified API.
That range is exactly what a cross-platform Nostr app needs. A web build may rely on a browser extension. An Android build may use Amber. A security-conscious user may prefer a NIP-46 bunker. A test app may use a local private key. The library can abstract over those choices enough that the product can stay coherent.
The danger is hiding too much. External signers require user approval and can leave operations pending. The docs expose pending request streams, local cancellation and rejection exceptions so app UI can show what is waiting, cancel stuck requests and explain remote rejection. A polished client should use that instead of leaving the user staring at a frozen button.
The accounts guide adds another boundary. Apps can log in with private keys, public keys for read-only accounts or external signers. It warns that bunker connection details must be stored locally if the user should reconnect without re-authenticating. That storage is sensitive and should be treated as account infrastructure, not casual app settings.
Relay auth should be a deliberate choice
Dart NDK handles NIP-42 relay authentication automatically. The docs describe a lazy auth strategy, which is the default, and an eager auth strategy. Lazy auth sends AUTH only after the relay responds with `auth-required`, then retries the original request. Eager auth signs immediately after a relay challenge.
The default is more privacy-respecting because it does not reveal the user's identity until the relay actually requires it. That is not just a technical preference. Authentication events can link a user to a relay interaction, and apps should avoid revealing identity earlier than necessary.
Eager auth can still be reasonable for relays that always require authentication. It can remove a round trip and make private relay access feel faster. The app needs to decide whether speed or privacy is the better default for the current surface.
Test both paths. Use a relay that never requires auth, one that requires auth after the first request and one that always challenges. Confirm timeout behavior, failure copy, reconnect behavior and whether the signer prompt clearly names the relay and action the user is approving.
NWC is inside the toolkit, not the whole toolkit
Dart NDK includes a Nostr Wallet Connect usecase, and the docs show the basic requirement: the app needs a `nostr+walletconnect://` URI from a wallet service provider. The examples connect with that URI, fetch a balance and pay a BOLT11 invoice.
The source goes further. The NWC implementation parses connection URIs, queries wallet info kind 13194 on the relays from the URI, subscribes to notifications and responses, records wallet permissions, understands supported versions and encryption tags, and can use either legacy or current notification kinds.
The URI parser supports one relay, multiple comma-separated relays and mixed `relay` plus `relays` parameters. That is a meaningful reliability improvement because an NWC connection can fail if the app and wallet service do not meet on a working relay.
NWC still moves real money. Dart NDK can expose `get_balance`, `pay_invoice`, `make_invoice`, `list_transactions`, `lookup_invoice`, hold-invoice methods and budget information. The app must show permissions clearly, handle wallet-side errors, protect connection strings and make sure a payment result is verified in the product layer before unlocking anything durable.
Zaps build on payment and signing
The zaps usecase shows how Dart NDK combines Nostr and Lightning pieces. The example uses an NWC connection, an LNURL identifier, a comment, an amount, a signer, relay URLs, a recipient public key and an optional event id, then waits for a pay-invoice response and, if requested, a Zap receipt.
That is useful because zaps are not only a payment. NIP-57 involves a Zap request event, an LNURL payment flow and a receipt that clients can validate and display. A library can reduce the glue code, but the app still has to explain what is being zapped and what counts as success.
The example also demonstrates why signer and wallet boundaries matter together. The wallet connection may pay the invoice. The signer may sign the Zap request. The relay layer may carry the receipt. A single button in the UI hides at least three trust surfaces.
Test zaps with tiny amounts, failed invoices, missing receipts, wrong relays, rejected signer requests, wallet permission denial and duplicate taps. If a product treats a zap as social feedback, the failure can be mildly annoying. If it gates access, the settlement and receipt logic needs a much stricter path.
Files use Blossom but storage still matters
The docs say the high-level files usecase uses Blossom underneath to get, upload and delete files. It also says the default user server list, kind 10063, is used for upload and delete. Builders who need more control can use the Blossom usecase directly.
That is a strong pattern for application code. Most products should use a high-level file API until they have a real reason to manage blob servers directly. A media-heavy app, however, will eventually care about server selection, upload progress, MIME checks, deletion behavior and how file references appear in Nostr events.
The files example downloads both a Blossom URL and a non-Blossom URL. That reminds readers that a client may be asked to fetch files from many places. The library can help, but the product still needs size limits, MIME validation, preview safety, retry rules and user-visible error handling.
Blossom storage is not relay storage. A relay can carry an event that points to a file, while a Blossom server stores the blob. A user trying to delete content may need both event handling and blob-server behavior to line up. Dart NDK gives tools for that path; it does not make every server trustworthy.
Cashu is explicitly experimental
The Cashu docs are unusually blunt: they mark the usecase experimental, say not to use it in production and warn that funds are lost if the user deletes the database by resetting the app because there is no recovery option. That warning should stay near any serious evaluation of Dart NDK wallets.
The usecase aims to manage ecash inside an application: adding mints, funding, redeeming, spending, receiving, checking balances and streaming pending or recent transactions. Those are valuable building blocks for Nostr-native money flows, but the current docs tell you the production line has not arrived.
The NDK config also has a `cashuUserSeedphrase` field and comments that the seed phrase allows full access to Cashu funds. That is not an abstract implementation detail. If an app stores or derives that seed, the storage, backup and deletion story becomes a money-safety story.
Use Cashu support as a development surface unless the project has separately audited the wallet model, recovery path, mint trust, proof storage, deletion behavior, database reset behavior and user warnings. The presence of an API is not permission to put user funds at risk.
Performance is a mobile requirement
The performance docs name two constraints: battery and compute on one side, network bandwidth on the other. That is exactly the Nostr mobile problem. A naive client can fetch too much from the wrong relays, verify too many signatures on the UI thread and make the app feel broken even when the protocol is technically working.
Dart NDK leans on inbox/outbox relay selection and cache reuse to reduce network waste. It can split user filters into smaller relay-tailored filters when it knows where information should be. That kind of behavior is the difference between a demo feed and a client that can survive a real follow list.
Signature verification is called out as the heavy compute operation. The docs recommend `RustEventVerifier()` for client applications because it uses a separate thread and is more performant. The getting-started page also lists Rust toolchain prerequisites for Android and iOS when using that verifier.
This means build pipelines matter. A Flutter team adopting Rust-backed verification needs to test local development, CI, Android targets, iOS device builds, simulators, web targets and fallback behavior. The fastest verifier in a docs page is not useful if the release build or app-store pipeline cannot reproduce it.
The architecture separates core and UI
The core `ndk` package supports Android, iOS, Linux, macOS, web and Windows through its Pub metadata. It depends on Dart networking, crypto, websocket, CBOR, BIP-340, Sembast and related libraries. The package is broad because Nostr protocol work touches many layers.
The `ndk_flutter` package is separate. Pub describes it as a Flutter UI package providing ready-to-use widgets, and its dependencies include Flutter, secure storage, URL launching, QR code rendering, mobile scanning, Amber support and NIP-07 signer integration. That is a different responsibility from core event plumbing.
This separation is healthy. A developer building a Dart command-line tool can avoid UI dependencies. A Flutter wallet surface can use widgets where helpful. A product can still replace or wrap the widgets if its own UX needs more control.
Do not treat the UI package as a substitute for understanding the core. A wallet-connect widget, signer widget or QR flow still asks for sensitive capabilities. The wrapper can make a path easier to present, but product teams should know which NDK usecase sits under the component.
What to test before building on it
Start with version pinning. Confirm the exact `ndk`, `ndk_flutter`, signer, storage and verifier package versions. Read the changelog for the chosen version, not only the latest docs. Build once on every platform you claim to support before relying on a feature in product copy.
Then test relays. Use known good relays, broken relays, slow relays, private relays requiring NIP-42, users with NIP-65 relay lists, users without them, new accounts, cache misses, cache hits, reconnects, app backgrounding and subscription cleanup through `ndk.destroy()`.
Test signer flows with a local BIP-340 signer, public-key read-only accounts, NIP-07, NIP-46 and Amber where relevant. Show pending requests. Cancel them. Reject them remotely. Confirm your UI does not sign or encrypt without a visible user action when a signer should ask.
Finally test money and storage separately. Exercise NWC balance, payment, invoice lookup and notification paths with tiny amounts. Treat zaps as a full NIP-57 flow. Keep Cashu out of production unless you have solved recovery. Check file downloads for size and MIME safety. Inspect cache reset behavior before telling users they can recover.
Where it fits in the ecosystem
Dart NDK is part of a broader family of Nostr libraries that move protocol complexity into maintained packages. The OpenSats report frames these libraries as the layer that lets apps spend less time rebuilding recurring infrastructure and more time shaping the user experience they want to offer.
Its distinct role is Flutter and Dart. TypeScript NDK serves web and JavaScript builders. Rust Nostr serves Rust and many bindings. NDKSwift serves Apple-platform developers. Dart NDK gives Flutter teams a comparable path in the environment where many mobile and cross-platform app builders already work.
The downstream list on the docs site matters because it shows the library is not only an isolated package. Projects such as Yana, Camelus, ZapStream and Zapstore push on social, media, commerce and distribution surfaces. Their needs help explain why the library includes both high-level usecases and lower-level hooks.
The ecosystem risk is monoculture. Good libraries make building easier, but apps should still understand the protocol boundaries they depend on. A bug in relay selection, signer state or wallet handling can travel downstream quickly. Use Dart NDK as a shared foundation, not as a reason to stop reading NIPs.
The practical close
Dart NDK is most useful when you see it as mobile Nostr infrastructure. It gives Flutter and Dart builders a serious starting point for relay selection, event verification, caching, signers, NWC payments, zaps, file handling and newer protocol usecases without pretending all of those pieces are solved product decisions.
Its strengths are the same places a Nostr app usually gets hard: choosing relays intelligently, keeping UI responsive, handling external signers, giving wallet flows a coherent API and keeping high-level usecases close to lower-level escape hatches. That is valuable work.
Its limits are equally clear. It is a young active project with open issues, dev releases ahead of stable, explicitly experimental Cashu support and many surfaces that touch sensitive user state. A library can reduce boilerplate, but it cannot choose the right permissions, explain signer prompts or design a recovery path for money.
Use Dart NDK with version discipline, visible signer UI, conservative wallet permissions, explicit cache policy and platform-specific testing. If those checks are in place, it can let a Flutter team focus on the app itself instead of spending months rebuilding the Nostr substrate before the first user ever sees a post.
Useful Nostr context
Use these pages to compare Dart NDK with adjacent SDK and protocol references.
- Dart Package: The nearby Dart ecosystem page for Nostr development.
- Flutter Package: Mobile app context for Flutter-based Nostr work.
- NDK: The JavaScript NDK reference point for naming and architecture comparison.
- Rust Nostr: A mature cross-language reference for Nostr SDK design.
- Developer Tools & Libraries: The broader shelf for SDKs, protocol helpers and implementation references.
- NIPs: Protocol standards and their product consequences.
- Keys and identity: A useful companion when an article touches signing, secrets or custody boundaries.
Sources worth opening
Start with the Dart NDK docs, GitHub repository, package README, Pub metadata and changelog. Then read the focused docs for gossip, persistence, signers, NWC, zaps, files, Cashu and the relevant NIPs before trusting it inside a mobile product.
- Dart NDK documentation site
- relaystr/ndk GitHub repository
- relaystr/ndk GitHub API metadata
- Dart NDK root README
- Dart NDK package README
- Dart NDK package pubspec
- Dart NDK package changelog
- Pub API metadata for ndk
- ndk package on Pub
- ndk changelog on Pub
- Dart NDK getting started
- Dart NDK getting started markdown
- Dart NDK enabling gossip guide
- Dart NDK persistence guide
- Dart NDK signers concept
- Dart NDK performance concept
- Dart NDK NIP-42 authentication concept
- Dart NDK accounts usecase
- Dart NDK user relay lists usecase
- Dart NDK NWC usecase
- Dart NDK NWC get balance example
- Dart NDK NWC pay invoice example
- Dart NDK NWC implementation
- Dart NDK NWC URI parser
- Dart NDK zaps usecase
- Dart NDK zap example
- Dart NDK files usecase
- Dart NDK files example
- Dart NDK Cashu usecase
- Dart NDK gift wrap usecase
- Dart NDK public exports
- Dart NDK config source
- Dart NDK main entry source
- Pub API metadata for ndk_flutter
- ndk_flutter package pubspec
- Pub API metadata for nip07_event_signer
- nip07_event_signer pubspec
- Pub API metadata for ndk_objectbox
- ndk_objectbox pubspec
- Pub API metadata for ndk_rust_verifier
- Dart NDK AI guide
- OpenSats report on Nostr libraries
- getAlby awesome-nwc list
- NIP-47 Nostr Wallet Connect
- NIP-65 relay list metadata
- NIP-42 authentication of clients to relays
- NIP-57 Lightning zaps
- NIP-59 gift wrap
- NIP-05 domain identifiers
- NIP-77 negentropy sync





