Building Immutable Notes on Logos: Phase 0
Rick
Why This Should Exist on Logos
Most note-taking apps are surveillance by default. Your thoughts live on someone else's server, readable by the company, accessible to governments, vulnerable to breaches. Even "encrypted" apps often hold the keys themselves.
Logos is built around a different premise entirely. No central servers. No company holding your keys. No permission required to participate. A notes app on Logos is an act of thought sovereignty. Your notes exist only for you, encrypted by a key that only you hold, with no intermediary ever seeing the plaintext.
The full vision is an encrypted Markdown notes manager with Keycard hardware key protection and sync across devices via Logos Messaging and Logos Storage. No accounts. No servers. Your recovery phrase is your identity. Each phase builds toward that — this post is about Phase 0.
More about the idea and all development phases is here.
Phase 0 goals:
- Import a BIP39 recovery phrase once to derive your encryption key
- Set a PIN that protects access — the mnemonic is never stored
- Write a single note (one note by design — scope kept minimal to get the crypto and module architecture right first)
- Lock and unlock with PIN only
- Run as a proper Logos App module, not a standalone app
A future phase will add a note list with a sidebar to select and manage multiple notes. For now, one note. Get the foundation right.
Getting Started
After the Logos ideas repo opened for community contributions, I looked for something lean enough to actually build but meaningful enough to matter. Encrypted notes with Keycard + Logos sync felt right — personal sovereignty, hardware security, decentralized infrastructure, all in one small app.
I started by cloning everything relevant:
- logos-app-poc — the Logos App shell, the Qt6/C++ host that loads modules
- logos-chat-ui — primary reference dapp, same C++/QML pattern
- logos-chat-module — core module reference
- logos-template-module — scaffold starting point
- logos-design-system — QML components and theme tokens, built by @khushboo9911
- status-desktop — the most mature app on this stack, invaluable for QML patterns
- nim-chat-poc — Logos Messaging C FFI reference
Environment: Ubuntu 24.04, Qt 6.9.3, CMake 3.28, libsodium 1.0.18, Nix 2.34.
For building and research I used Claude Code (Sonnet). The collaboration felt like a hackathon — I brought the roadmap, the UX direction, the architectural decisions, and the links. Claude Code felt like an enormously fast developer who could read fifty source files and write correct C++ in the time it takes to make coffee. The discipline that made it work: explore first, understand the contracts, then implement.
The Crypto Architecture
One principle drives the whole design: nothing sensitive ever touches disk in plaintext.
On first import:
- You enter your BIP39 recovery phrase
- Argon2id derives a 256-bit master key using a deterministic salt —
SHA-256(mnemonic)— same phrase always produces the same key, no salt storage needed - A fresh random 16-byte salt is generated
- Argon2id derives a PIN wrapping key from your PIN + that random salt
- AES-256-GCM encrypts the master key with the PIN wrapping key
- SQLite stores: wrapped key blob + nonce + PIN salt. The mnemonic is gone.
On every save: Note content is AES-256-GCM encrypted with the master key and a fresh random nonce. Only ciphertext + nonce hit the database.
On lock:
sodium_memzero wipes the master key from memory. The note becomes unreadable — even by the app itself.
On unlock: Load the wrapped blob, re-derive the PIN wrapping key, decrypt. Wrong PIN causes GCM authentication tag failure — the encrypted blob produces garbage, not a partial result. There is no way to test guesses and measure how close you are.
The DatabaseManager schema is deliberately minimal:
notes(id, ciphertext, nonce)
wrapped_key(id, ciphertext, nonce, pin_salt)
meta(key, value)
An attacker with full disk access gets encrypted blobs and nothing else.
How Logos App Modules Work
This was the most valuable discovery of Phase 0. The Logos App is a microkernel — it loads modules dynamically, each as a Qt plugin. Three types exist:
| Type | Mechanism | Use case |
|---|---|---|
core |
C++ .so implementing PluginInterface |
Backend logic, crypto, storage |
ui |
C++ .so implementing IComponent |
Full UI with push events |
ui_qml |
Plain .qml file, no C++ needed |
Simple UI, synchronous calls only |
We built two modules:
notes(type:core) — NotesPlugin.cpp wraps the backendnotes_ui(type:ui_qml) — Main.qml, three screens
The plugin contract:
class NotesPlugin : public QObject, public PluginInterface {
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.logos.NotesModuleInterface" FILE "plugin_metadata.json")
Q_INTERFACES(PluginInterface)
public:
Q_INVOKABLE void initLogos(LogosAPI* api);
Q_INVOKABLE bool initialize();
Q_INVOKABLE QString isInitialized();
Q_INVOKABLE QString importMnemonic(QString mnemonic, QString pin, QString confirm);
Q_INVOKABLE QString unlockWithPin(QString pin);
Q_INVOKABLE QString loadNote();
Q_INVOKABLE QString saveNote(QString text);
Q_INVOKABLE QString lockSession();
signals:
void eventResponse(const QString& eventName, const QVariantList& data);
};
The QML bridge is synchronous-only. logos.callModule() blocks, returns JSON, done. No signals, no push events for ui_qml type. For notes this is sufficient — everything is user-initiated. Phase 2 sync will require moving to a type: "ui" C++ plugin with full LogosAPI* access.
Installation layout:
~/.local/share/Logos/LogosApp/modules/notes/
├── manifest.json
└── notes_plugin.so
~/.local/share/Logos/LogosApp/plugins/notes_ui/
├── manifest.json
└── Main.qml
Key Challenges
Silent module load failure
Clicking "Load" did nothing. No error message. The log said: "Module not found in known plugins: notes".
Root cause: Qt embeds plugin metadata in the .so binary via Q_PLUGIN_METADATA. Our plugin_metadata.json was {} — empty. The shell reads that embedded section to register the plugin. Empty metadata means it never registers. Fill it with the full module manifest and the IID set to "org.logos.NotesModuleInterface". Problem solved.
QML import sandbox
ui_qml plugins run in a sandboxed QQuickWidget. import Logos.Theme and import Logos.Controls fail silently inside it. We hardcoded the Logos dark palette hex values directly in the QML for Phase 0. Not elegant, but correct. Phase 1 will move to a type: "ui" C++ plugin which has full import path access.
Screen state after tab reload
Closing the tab unloads the module. Reopening showed Import instead of Unlock — in-memory state was lost. Fix: isInitialized() reads from the database, not from memory. The DB persists across module loads; memory doesn't.
Reset not clearing in-memory state
After reset, reopening still showed Unlock. The database was wiped but the plugin instance retained its in-memory state. Root cause: resetAndWipe() existed on NotesBackend but was never exposed as a Q_INVOKABLE on NotesPlugin. A method invisible to the QML bridge silently does nothing. Always expose what the UI needs to call.
What's Next
Phase 1 — Keycard
Swap Argon2id software key derivation for Keycard hardware key derivation. Same PIN UX, same SQLite schema, no data migration needed. The mnemonic-derived key pair maps directly to Keycard's identity model.
Phase 2 — Logos Messaging + Storage (Research)
This is active R&D territory. storage_module is already installed and running in the Logos App. The nim-chat-poc C FFI is understood. But the exact design is still open.
What we know: Logos Storage will store encrypted blobs locally on your running node and returns a CID. How that CID propagates to other nodes for redundancy and backup — that's part of what Phase 2 exploration will answer. Logos Messaging is the likely transport layer for syncing note state across devices, but the protocol design is TBD.
Building this has been an adventure. Every phase reveals something unexpected — about the platform, about cryptography, about what "simple" actually requires under the hood. The learning is dense and the satisfaction is real. There is something particular about building infrastructure for sovereignty — it doesn't feel like shipping a feature. It feels like laying stone.
Clone it, break it, silently or with feedback. Find me on Status or in the Logos Discord.
github.com/xAlisher/logos-notes
"We must defend our own privacy if we expect to have any." — Eric Hughes, A Cypherpunk's Manifesto, 1993