Skip to content

Passnote

End-to-end encrypted, server-less, local-first sync for Dart and Flutter.

Passnote lets a small group of devices share a live, mergeable dataset with no server and no account. Every device holds a full copy and works offline; new devices join with a short one-time code; a lost device can be revoked. No plaintext ever leaves a device.

final passnote = await passnoteBuilder(
  appId: 'shopping',
  salt: 'shopping:v1',
).open();

final db = await passnote.openDatabase('home', passphrase: words);
final items = db.collection<Item>('items', Item.codec);

await items.put(Item(id: newId(), name: 'Milk'));
items.watch().listen((all) => print('now ${all.length} items'));

Why Passnote

  • End-to-end encrypted. XChaCha20-Poly1305 on every byte on the wire and SQLCipher for the files at rest (verified on open, never assumed), with keys that never leave the devices and a forward-secrecy ratchet on every connection.
  • Real device membership. Each device has its own keypair. Joining is a nine-digit code over a PAKE; leaving is a revocation that rotates the group key away from the lost device. No shared password to rotate by hand.
  • Server-less. Devices find each other over the local network (or an explicit WebSocket address, for browsers) and sync directly. Nothing to deploy, nothing to breach centrally.
  • Local-first. Reads and writes never wait for the network. Concurrent edits merge deterministically; counters add up and sets keep everyone's changes when you ask for it.
  • Self-healing sync. Changes push immediately; a periodic fingerprint exchange finds and repairs any gap, however old, in a few round trips.
  • Small, swappable parts. Storage, transport, key store, cipher, identity, and PAKE are ports with production adapters; replace any of them without touching app code.

How it fits together

flowchart LR
  App["Your app"] --> F["passnote_flutter<br/>builder + platform glue"]
  F --> Core["passnote<br/>engine, CRDTs, protocol"]
  F --> Crypto["passnote_crypto<br/>XChaCha20, Argon2id, SPAKE2"]
  F --> Lan["passnote_lan<br/>mDNS + WebSocket"]
  Gen["passnote_generator<br/>codecs from annotations"] -. build_runner .-> App

Packages

Package Purpose
passnote_flutter The entry point for Flutter apps: passnoteBuilder, keychain key store, notifiers. Re-exports everything below.
passnote The pure-Dart engine: collections, CRDTs, queries, the sync protocol, the SQLite store.
passnote_crypto XChaCha20-Poly1305, Argon2id, BIP39, SPAKE2, the device identity.
passnote_lan Transports: mDNS LAN discovery (native) and WebSockets (everywhere), plus the reference relay.
passnote_generator Generates codecs and CRDT schemas from @Document() annotations.

Next steps

  • Start here: what Passnote is, in one screen.
  • Tutorial: empty app to two synced devices.
  • Concepts: the data model, clocks, sync, identity.
  • Security: what is protected, and the boundaries.
  • Limits: honest numbers on group size and data volume.
  • API reference: generated dartdoc for every symbol, per package on pub.dev.