<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Kornel Maraz</title>
    <description>The latest articles on Forem by Kornel Maraz (@kornel_maraz_5e66a3e4e27d).</description>
    <link>https://forem.com/kornel_maraz_5e66a3e4e27d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2501031%2F292ca222-73f0-4722-a39b-2263145e78ef.jpg</url>
      <title>Forem: Kornel Maraz</title>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kornel_maraz_5e66a3e4e27d"/>
    <language>en</language>
    <item>
      <title>Privacy-first mind mapping app. Part 6: Maintainability and Coding Rules</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Wed, 29 Apr 2026 21:00:49 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-6-maintainability-and-coding-rules-1669</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-6-maintainability-and-coding-rules-1669</guid>
      <description>&lt;p&gt;This is the chapter where I admit something simple: clean code talks are nice, but products are built in history, not in slides.&lt;/p&gt;

&lt;p&gt;When MindMapVault was small, I could hold most of it in my head. Then the project grew into frontend work, desktop work, encryption flows, uploads, backend routes, different database paths, deployment scripts, release notes, and a lot of "just fix this one thing" days.&lt;/p&gt;

&lt;p&gt;That is when coding rules stopped being theory and became survival.&lt;/p&gt;

&lt;p&gt;The rules I kept coming back to were not fancy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep changes small&lt;/li&gt;
&lt;li&gt;do not refactor the whole house when the sink is leaking&lt;/li&gt;
&lt;li&gt;be extra careful in crypto, auth, and storage code&lt;/li&gt;
&lt;li&gt;keep frontend and backend contracts aligned&lt;/li&gt;
&lt;li&gt;prefer readable code over clever code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds obvious. It is also the difference between a project that can still move and a project that starts breaking under its own weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code has a visible history, and that is normal
&lt;/h2&gt;

&lt;p&gt;You can see the project history in the codebase. I actually think that is healthy to admit.&lt;/p&gt;

&lt;p&gt;MindMapVault started with MongoDB. Later I added Stoolap. Later I added SQL-oriented paths and those &lt;code&gt;_sql.rs&lt;/code&gt; files. If you do not stop everything and rewrite the whole project from zero every time the architecture evolves, signs of the older path stay visible.&lt;/p&gt;

&lt;p&gt;That is not a moral failure. That is what real software looks like.&lt;/p&gt;

&lt;p&gt;You can often read the timeline directly from the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frontend_app/        hosted app UI, editor, crypto helpers, vault flows
frontend_www/        marketing site, release notes, public blog
desktop/src-tauri/   local desktop shell and native packaging
backend/src/         auth, routes, storage, DB adapters, upload flows
scripts/             regression runners, banner rendering, deployment helpers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inside the backend there is another layer of history:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;older MongoDB-oriented paths&lt;/li&gt;
&lt;li&gt;later SQL and Stoolap paths&lt;/li&gt;
&lt;li&gt;route files that had to stay practical while the storage model evolved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can absolutely see that evolution if you read the repo for long enough. I am fine with that. Every long-running project carries some residue of its earlier decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical first, perfect never
&lt;/h2&gt;

&lt;p&gt;I like practical things. I like jobs done.&lt;/p&gt;

&lt;p&gt;That preference is visible in the code.&lt;/p&gt;

&lt;p&gt;Some parts are tidy and stable. Some parts are tactical. Some parts are not how I would design them in a greenfield rewrite. But a real product is not rebuilt from first principles every Tuesday morning.&lt;/p&gt;

&lt;p&gt;There is a version of software advice that pretends all good code emerges from calm, linear planning. That is not how most product work happens.&lt;/p&gt;

&lt;p&gt;Real code grows under pressure from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feature delivery&lt;/li&gt;
&lt;li&gt;production bugs&lt;/li&gt;
&lt;li&gt;changed infrastructure&lt;/li&gt;
&lt;li&gt;new storage backends&lt;/li&gt;
&lt;li&gt;packaging and deployment headaches&lt;/li&gt;
&lt;li&gt;the need to keep existing users working while the internals evolve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So yes, theoretically clean code and real-life code are often different things.&lt;/p&gt;

&lt;p&gt;The goal was never to make MindMapVault look like a textbook. The goal was to keep it understandable enough, safe enough, and changeable enough while the product kept moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the rules were bent
&lt;/h2&gt;

&lt;p&gt;There are places where the project is cleaner than average, and places where it absolutely is not.&lt;/p&gt;

&lt;p&gt;The most visible compromises are usually these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Boundaries are not always perfect&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some concerns leak across layers because the fastest safe fix was not always the prettiest abstraction.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;React hygiene is not always textbook&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are places with deliberate lint-rule exceptions or dependency-array compromises because stable behavior in a real flow mattered more than satisfying the purest interpretation of the rule.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Style consistency is uneven&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some modules were written during calmer phases. Others were written during "let me finally this *** and go to the bed" phases. That difference is visible.&lt;/p&gt;

&lt;p&gt;I would rather say that openly than write fake architecture prose around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copilot changed the texture of the code too
&lt;/h2&gt;

&lt;p&gt;Another honest point: code does not look like it did five years ago.&lt;/p&gt;

&lt;p&gt;This project carries signs of Copilot use and, more broadly, LLM-assisted development. That is real now. We should stop pretending otherwise.&lt;/p&gt;

&lt;p&gt;Sometimes that means faster scaffolding. Sometimes it means a strange but useful first draft. Sometimes it means the code gets a little more chaotic or stylistically mixed than it would with one human colleague writing every line in one voice.&lt;/p&gt;

&lt;p&gt;But there is another side to that trade-off.&lt;/p&gt;

&lt;p&gt;LLMs are also good at searching through that mess, finding the right file, spotting a broken path, or repairing a repeated pattern faster than a human might. In that sense, the code is not only written differently now. It is also maintained differently now.&lt;/p&gt;

&lt;p&gt;I think we have to accept both sides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Copilot touch is visible&lt;/li&gt;
&lt;li&gt;some generated structure is less elegant than an ideal hand-crafted version&lt;/li&gt;
&lt;li&gt;but the same tooling also makes large, messy codebases easier to search, patch, and recover&lt;/li&gt;
&lt;li&gt;and different coding styles are visible in a single project even one man´s hand (and a robot´s hand)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is part of modern software reality now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually kept the project under control
&lt;/h2&gt;

&lt;p&gt;The workflow was practical, not ceremonial.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tsc&lt;/code&gt; and production builds were the first fast safety net.&lt;/li&gt;
&lt;li&gt;backend regression runs through WSL and Python scripts were the real "did I break the product" check&lt;/li&gt;
&lt;li&gt;dependency audits were hygiene, not proof of quality&lt;/li&gt;
&lt;li&gt;security-sensitive changes were validated by behavior, not by vibes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the Python side I leaned on repeatable checks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scripts/backend_regression_test.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/attachement_regression_test.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/shared_regression_test.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/production_functional_test.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For burst and stress behavior I also used helpers like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scripts/load_test_stoolap.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/production_burst_test.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts/crud_burst_runner.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not make the project magically clean. It just means there were repeatable ways to keep reality in view.&lt;/p&gt;

&lt;p&gt;Passing lint does not prove good architecture. A green build does not prove good UX. An audit does not prove safe design.&lt;/p&gt;

&lt;p&gt;But together, those checks helped keep the project from drifting too far into chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintainability standard I actually believe in
&lt;/h2&gt;

&lt;p&gt;For me, maintainability is not "could this win a code-style argument on the internet?"&lt;/p&gt;

&lt;p&gt;It is more practical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can I still understand this at 2 AM during a bug&lt;/li&gt;
&lt;li&gt;can I change one thing without breaking five others&lt;/li&gt;
&lt;li&gt;can I trace a storage or auth path end to end&lt;/li&gt;
&lt;li&gt;can I ship a fix without turning it into a rewrite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the bar I care about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mindmapvault.com/" rel="noopener noreferrer"&gt;MindMapVault&lt;/a&gt; is not pristine, and I do not need to pretend it is. It is a real product with a real timeline, visible scars, and a codebase that shows both human shortcuts and AI-era development habits.&lt;/p&gt;

&lt;p&gt;I am okay with that.&lt;/p&gt;

&lt;p&gt;What matters is that the important parts stay understandable, the dangerous parts stay guarded, and the project keeps moving without collapsing under its own history.&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>devjournal</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Privacy-first mind mapping app. Part 5: UI / Keyboard First</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Sun, 26 Apr 2026 12:51:19 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-5-ui-keyboard-first-4797</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-5-ui-keyboard-first-4797</guid>
      <description>&lt;p&gt;Part 0 explained the feeling I wanted to recover: thought velocity.&lt;/p&gt;

&lt;p&gt;This chapter is where that feeling turns into UI decisions.&lt;/p&gt;

&lt;p&gt;I did not optimize for feature count first. I optimized for interruption cost. Every extra click, hidden mode, or ambiguous icon steals cognitive bandwidth from the idea itself.&lt;/p&gt;

&lt;p&gt;So the UI direction was keyboard-first and friction-minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;common actions reachable without mouse travel&lt;/li&gt;
&lt;li&gt;fast node creation and editing loops&lt;/li&gt;
&lt;li&gt;predictable, stable interaction patterns&lt;/li&gt;
&lt;li&gt;visual structure that supports scanning, not decoration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A keyboard-first interface is not nostalgia. It is a throughput strategy.&lt;/p&gt;

&lt;p&gt;When a person is deep in a problem, they should not negotiate with the UI. The interface should be almost mechanical, so attention stays on thinking.&lt;/p&gt;

&lt;p&gt;There is always tension here.&lt;/p&gt;

&lt;p&gt;Modern apps reward adding controls, toggles, and contextual actions everywhere. Some are useful. Too many create noise. For this product, I repeatedly chose less surface area when that preserved speed.&lt;/p&gt;

&lt;p&gt;The second tension is accessibility versus complexity.&lt;/p&gt;

&lt;p&gt;Keyboard-first does not mean mouse-hostile. It means every key path should be meaningful, discoverable, and reliable. It also means visual focus states, labels, and interaction feedback must stay clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontend backbone we built on
&lt;/h2&gt;

&lt;p&gt;The core model for the editor is built on the React Flow ecosystem (&lt;code&gt;@xyflow/react&lt;/code&gt;). For this project, it was the most reliable and practical foundation I found for a mind-map-style UI.&lt;/p&gt;

&lt;p&gt;Project link: &lt;a href="https://reactflow.dev/" rel="noopener noreferrer"&gt;https://reactflow.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This mattered a lot because the whole frontend editor is effectively built around that graph viewport model. In simple terms, the canvas/viewport layer handles the graph rendering and interaction surface, and most of our product-specific work sits on top of it.&lt;/p&gt;

&lt;p&gt;In practice, a big part of the app is then "buttons and keyboard actions talking to the graph canvas":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating and deleting nodes&lt;/li&gt;
&lt;li&gt;connecting and re-ordering structure&lt;/li&gt;
&lt;li&gt;focusing, selecting, and navigating&lt;/li&gt;
&lt;li&gt;applying styling and metadata actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That may sound obvious, but this choice was a huge helper. It gave the project a stable backbone early, so I could spend energy on product behavior (keyboard speed, encryption-aware flows, vault UX) instead of rebuilding graph rendering primitives from scratch.&lt;/p&gt;

&lt;p&gt;In practical product terms, this chapter connects directly to trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast interaction encourages regular use&lt;/li&gt;
&lt;li&gt;regular use creates real workflow dependence&lt;/li&gt;
&lt;li&gt;dependence only happens when the tool gets out of the way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is still the bar for MindMapVault.&lt;/p&gt;

&lt;p&gt;If a user has to think about the interface more than the idea, the product failed for that moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thanks and credits
&lt;/h2&gt;

&lt;p&gt;This part of the project also sits on top of a lot of other people's work.&lt;/p&gt;

&lt;p&gt;Huge thanks to the maintainers, contributors, communities, and companies behind React, React Flow, Tauri, TypeScript, Vite, Tailwind, Zustand, the Rust ecosystem, and the many smaller libraries that made this UI direction realistic for an independent product.&lt;/p&gt;

&lt;p&gt;React Flow deserves a specific mention here because it gave the editor a serious, practical backbone early. That let me spend time on keyboard speed, interaction clarity, and product behavior instead of rebuilding graph rendering and viewport interaction from zero.&lt;/p&gt;

&lt;p&gt;There is a fuller acknowledgement and licensing summary here: &lt;a href="https://www.mindmapvault.com/CREDITS.md" rel="noopener noreferrer"&gt;MindMapVault credits and licenses&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>productivity</category>
      <category>ui</category>
      <category>ux</category>
    </item>
    <item>
      <title>Privacy-first mind mapping app. Part 4: Zero Knowledge and Private Thought</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Sat, 25 Apr 2026 16:37:46 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-4-zero-knowledge-and-private-thought-4jf5</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-4-zero-knowledge-and-private-thought-4jf5</guid>
      <description>&lt;p&gt;&lt;em&gt;This chapter explains why zero knowledge is not marketing decoration for MindMapVault. It is a product boundary. It also explains the limits that come with that choice: no admin rescue, no silent recovery, and no backend access to your private thinking space.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Zero knowledge is often discussed like a technical badge.&lt;/p&gt;

&lt;p&gt;For this product, it is more practical than that.&lt;/p&gt;

&lt;p&gt;It answers a simple question: who is allowed to see the raw material of your thinking?&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://www.mindmapvault.com/" rel="noopener noreferrer"&gt;MindMapVault&lt;/a&gt;, the answer is supposed to be very small by default: you, and only the people you deliberately share with.&lt;/p&gt;

&lt;p&gt;That matters because a notes or mind-map vault is not just storage. It is where unfinished thoughts live before they are ready for other people.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a notes or mind-map vault really is
&lt;/h2&gt;

&lt;p&gt;A personal notes or mind-map vault is not just a database with text attached.&lt;/p&gt;

&lt;p&gt;It is an externalized thinking space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unfinished thoughts&lt;/li&gt;
&lt;li&gt;personal beliefs and doubts&lt;/li&gt;
&lt;li&gt;raw ideas before they are safe to share&lt;/li&gt;
&lt;li&gt;private health, emotional, financial, or creative material&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important property here is psychological safety, not only confidentiality.&lt;/p&gt;

&lt;p&gt;In many products, the most relevant observer is not necessarily a hacker. It is the platform itself being able to read, analyze, index, summarize, or repurpose what you wrote.&lt;/p&gt;

&lt;p&gt;That is exactly where zero knowledge makes sense.&lt;/p&gt;

&lt;p&gt;If the system cannot read the vault content, it cannot quietly become an interpreter of your inner workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why zero knowledge fits notes and mind maps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Thinking requires freedom from observation
&lt;/h3&gt;

&lt;p&gt;If users know a provider can read their notes, even if the company promises not to, behavior changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;people self-censor&lt;/li&gt;
&lt;li&gt;sensitive thoughts never get written down&lt;/li&gt;
&lt;li&gt;creative and analytical depth drops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero knowledge changes that condition by making access technically unavailable to the platform rather than merely disallowed by policy.&lt;/p&gt;

&lt;p&gt;That difference matters. A promise can change. An architecture boundary is harder to erode casually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most note creation starts alone
&lt;/h3&gt;

&lt;p&gt;Notes and mind maps are usually personal first and collaborative second.&lt;/p&gt;

&lt;p&gt;That matches the trade-offs of zero knowledge unusually well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no central recovery by default&lt;/li&gt;
&lt;li&gt;no admin access to read content&lt;/li&gt;
&lt;li&gt;no implicit sharing model hidden behind team tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is acceptable precisely because authorship and ownership are personal at the moment of creation.&lt;/p&gt;

&lt;p&gt;The raw draft stage is where privacy matters most.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sensitivity is long-lived
&lt;/h3&gt;

&lt;p&gt;The sensitive life of a note is often measured in years, not minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;therapy notes&lt;/li&gt;
&lt;li&gt;political beliefs&lt;/li&gt;
&lt;li&gt;legal planning&lt;/li&gt;
&lt;li&gt;business strategy&lt;/li&gt;
&lt;li&gt;research ideas that are not ready to leave the notebook yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero knowledge protects not only against current breaches, but also against future breaches, future policy changes, and future business pressure to mine user content.&lt;/p&gt;

&lt;p&gt;That is one of the strongest reasons to use it for a personal vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters even more for mind maps
&lt;/h2&gt;

&lt;p&gt;Mind maps are not just notes with boxes.&lt;/p&gt;

&lt;p&gt;The structure itself is sensitive.&lt;/p&gt;

&lt;p&gt;Relationships between nodes, hierarchy, clustering, and sequence often reveal more than a single paragraph would. The shape of a map exposes what you connect, what you prioritize, what you fear, and what you still have not resolved.&lt;/p&gt;

&lt;p&gt;So when I say &lt;a href="https://www.mindmapvault.com/" rel="noopener noreferrer"&gt;MindMapVault&lt;/a&gt; protects private thought, I do not only mean note bodies.&lt;/p&gt;

&lt;p&gt;I mean the broader cognitive structure you are building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;titles&lt;/li&gt;
&lt;li&gt;notes&lt;/li&gt;
&lt;li&gt;links between ideas&lt;/li&gt;
&lt;li&gt;hierarchy and grouping&lt;/li&gt;
&lt;li&gt;attachments connected to a map&lt;/li&gt;
&lt;li&gt;the evolving graph of what you are trying to understand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a mind-mapping product, zero knowledge is not only about securing text. It is about securing the structure of thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where zero knowledge adds practical technical value
&lt;/h2&gt;

&lt;p&gt;Zero knowledge becomes especially useful when the product supports features like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cross-device sync without server-side content visibility&lt;/li&gt;
&lt;li&gt;offline-first creation and later encrypted synchronization&lt;/li&gt;
&lt;li&gt;encrypted relationships between notes, maps, and attachments&lt;/li&gt;
&lt;li&gt;client-side processing for search, previews, or graph operations when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend can still do important work: identity, quotas, object versioning, billing boundaries, upload confirmation, ownership checks, and encrypted blob storage.&lt;/p&gt;

&lt;p&gt;What it should not do by default is interpret the content of your vault.&lt;/p&gt;

&lt;p&gt;That is the core split.&lt;/p&gt;

&lt;h2&gt;
  
  
  What zero knowledge does not give you
&lt;/h2&gt;

&lt;p&gt;This is the part that many products soften in marketing copy, but it should be said directly.&lt;/p&gt;

&lt;p&gt;Zero knowledge removes some kinds of convenience by design.&lt;/p&gt;

&lt;p&gt;If the provider does not have the keys, then the provider also cannot magically recover encrypted content for you later.&lt;/p&gt;

&lt;p&gt;That means users should not expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;admin access to unlock their vault&lt;/li&gt;
&lt;li&gt;support staff to read content and restore it on request&lt;/li&gt;
&lt;li&gt;silent password reset that decrypts old data&lt;/li&gt;
&lt;li&gt;organization-wide legal hold or audit visibility for private vault contents&lt;/li&gt;
&lt;li&gt;seamless team governance over data the server cannot read&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not product omissions caused by lack of engineering effort.&lt;/p&gt;

&lt;p&gt;They are direct consequences of the trust model.&lt;/p&gt;

&lt;p&gt;If a company can always recover your private vault, then the system is not truly zero knowledge in the strongest sense. It means someone, somewhere, holds a privileged path around your control.&lt;/p&gt;

&lt;p&gt;MindMapVault deliberately avoids presenting that kind of hidden escape hatch as a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where zero knowledge is not the right fit
&lt;/h2&gt;

&lt;p&gt;Zero knowledge is weaker as a default model when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collaboration is the primary mode from day one&lt;/li&gt;
&lt;li&gt;an organization requires admin oversight, legal holds, or auditability&lt;/li&gt;
&lt;li&gt;account recovery and delegated access are mandatory business features&lt;/li&gt;
&lt;li&gt;hand-over and continuity matter more than personal autonomy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why many enterprise knowledge platforms do not fully adopt this model.&lt;/p&gt;

&lt;p&gt;They optimize for continuity, governance, and administrative control. Those are legitimate priorities, but they are different priorities.&lt;/p&gt;

&lt;p&gt;MindMapVault is intentionally optimized for private creation first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I use this model anyway
&lt;/h2&gt;

&lt;p&gt;The short answer is that it matches the job the product is supposed to do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mindmapvault.com/" rel="noopener noreferrer"&gt;MindMapVault&lt;/a&gt; is meant to hold thoughts before they become presentation material.&lt;/p&gt;

&lt;p&gt;At that stage, the most important thing is not collaboration polish. It is freedom to think clearly without platform observation.&lt;/p&gt;

&lt;p&gt;That freedom is valuable enough that I accept the cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more complex client-side cryptography&lt;/li&gt;
&lt;li&gt;fewer recovery shortcuts&lt;/li&gt;
&lt;li&gt;stricter limits on backend convenience features&lt;/li&gt;
&lt;li&gt;more discipline in how storage and sharing are designed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To me, that is a reasonable trade.&lt;/p&gt;

&lt;p&gt;If the product is supposed to protect cognitive ownership, then the provider should not sit inside the room where the thinking happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  One distinction worth keeping clear
&lt;/h2&gt;

&lt;p&gt;Zero knowledge is not mainly about hiding notes.&lt;/p&gt;

&lt;p&gt;It is about protecting the thinking process itself.&lt;/p&gt;

&lt;p&gt;Once a thought becomes ready, users can still export it, publish it, share it, or move it into collaborative tools.&lt;/p&gt;

&lt;p&gt;But the raw workspace where ideas are born benefits from being cryptographically private.&lt;/p&gt;

&lt;p&gt;That is especially true for mind maps, where the structure can be as revealing as the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Zero knowledge makes sense for notes and mind maps because they are not just data stores. They are private cognitive spaces, and removing the platform's ability to observe that space changes how freely people think.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>privacy</category>
      <category>productivity</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Privacy-first mind mapping app. Part 3: Encryption Model - The Heart</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Fri, 24 Apr 2026 17:10:21 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-3-encryption-model-the-heart-43m5</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-3-encryption-model-the-heart-43m5</guid>
      <description>&lt;p&gt;&lt;em&gt;This article describes the encryption model behind MindMapVault — not as a feature list, but as an engineering constraint that shapes every other decision in the system. It assumes familiarity with basic encryption concepts and focuses on boundary decisions, not cryptographic primers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;MindMapVault is not mainly a drawing tool. It is an encryption discipline wrapped in a usable interface.&lt;/p&gt;

&lt;p&gt;If this model fails, no amount of UI, performance, or feature depth can compensate. Everything else becomes incidental.&lt;/p&gt;

&lt;p&gt;The model is simple to describe and hard to keep clean in real code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sensitive content is encrypted on the client&lt;/li&gt;
&lt;li&gt;the backend stores encrypted payloads and encrypted metadata&lt;/li&gt;
&lt;li&gt;plaintext notes and map content are not shipped to the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds obvious. The hard part is consistency across all features.&lt;/p&gt;

&lt;p&gt;Every new capability wants to poke a hole in the model: previews, sharing, search, exports, diagnostics, support tooling. Each one can accidentally reintroduce plaintext handling if you are not careful.&lt;/p&gt;

&lt;p&gt;The rule that saved me repeatedly was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a feature needs plaintext, it must be justified at the edge and never become backend default behavior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practical terms, that affected route design, payload structures, and attachment workflows. The backend handles version pointers, upload confirmation state, ownership checks, and object metadata. It should not become the place where private thought content is interpreted.&lt;/p&gt;

&lt;p&gt;There is also a human reason for this model.&lt;/p&gt;

&lt;p&gt;Privacy here is not about hiding wrongdoing. It is about cognitive ownership. Mind maps tend to capture half‑finished ideas, rough drafts, personal plans, and structures that are not yet coherent — and may never be published. Those thoughts should remain under your control unless you explicitly choose otherwise.&lt;/p&gt;

&lt;p&gt;This model is stricter than many products, and that raises implementation cost.&lt;/p&gt;

&lt;p&gt;I accepted that cost because it is the product promise.&lt;/p&gt;

&lt;p&gt;Everything else in this series is connected to this chapter: backend choices, UI speed decisions, coding rules, and even business trade-offs. If the encryption model becomes optional, the project loses its identity.&lt;/p&gt;

&lt;p&gt;For the full security model write-up, see the whitepaper: &lt;a href="https://www.mindmapvault.com/SECURITY.md" rel="noopener noreferrer"&gt;Security Whitepaper&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Concrete modules and libraries used
&lt;/h2&gt;

&lt;p&gt;On the TypeScript side, the encryption stack is explicit and modular, not hidden in one giant helper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web Crypto API (&lt;code&gt;crypto.subtle&lt;/code&gt;) for AES-256-GCM and SHA-256 operations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@noble/curves&lt;/code&gt; (&lt;code&gt;x25519&lt;/code&gt;) for the classical ECDH part&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@noble/post-quantum/ml-kem&lt;/code&gt; (&lt;code&gt;ml_kem768&lt;/code&gt;) for post-quantum KEM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@noble/hashes&lt;/code&gt; (&lt;code&gt;hkdf&lt;/code&gt;, &lt;code&gt;sha256&lt;/code&gt;) for key derivation and context-separated keys&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hash-wasm&lt;/code&gt; (&lt;code&gt;argon2id&lt;/code&gt;) for passphrase and master-key derivation paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practical terms, this is how I used it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) AES-256-GCM envelope (nonce + ciphertext+tag)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tagLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is used in the frontend crypto module to encrypt map payloads, titles, and attachment-related blobs before upload.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Hybrid key encapsulation (X25519 + ML-KEM-768)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;classicalShared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x25519&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSharedSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ephPrivate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recipientClassicalPub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cipherText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pqCiphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sharedSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pqShared&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;ml_kem768&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encapsulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipientPqPub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combinedKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hkdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;classicalShared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pqShared&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypt-mind-dek-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I combine classical and post-quantum shared secrets, derive a 32-byte wrapping key via HKDF, and use that to wrap the random DEK used for the actual vault content encryption.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Argon2id for passphrase-bound keys
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;argon2id&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;m_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hashLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outputType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;binary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is used for deriving high-cost keys from user secrets (for example, master key and share-passphrase flows), so brute-force resistance is parameterized and explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How encrypted sharing works without breaking zero knowledge
&lt;/h2&gt;

&lt;p&gt;This was one of the places where the model could have fallen apart very easily.&lt;/p&gt;

&lt;p&gt;Sharing is currently implemented as an encrypted export flow, not as live collaborative editing. That distinction matters. The backend can distribute a protected snapshot, but it still should not learn the map content or the share passphrase.&lt;/p&gt;

&lt;p&gt;The rough flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner browser
    |
    | 1. Serialize current vault snapshot { title, tree, exported_at, source_vault_id }
    | 2. Generate random salt
    | 3. Derive share key with Argon2id(passphrase, salt, params)
    | 4. Encrypt snapshot with AES-256-GCM using that derived share key
    | 5. Upload ciphertext + encryption_meta + checksum + expiry/hint metadata
    v
Backend / object storage
    |
    | Stores only:
    | - encrypted blob
    | - kdf metadata (salt, memory, iterations, parallelism)
    | - checksum, content type, expiry, hint, share id
    | Never stores:
    | - plaintext map
    | - plaintext notes
    | - share passphrase
    | - decrypted attachments
    v
Recipient browser
    |
    | 6. Fetch encrypted blob + encryption_meta by share URL
    | 7. User enters passphrase locally in the browser
    | 8. Derive the same share key locally with Argon2id
    | 9. Decrypt locally with AES-256-GCM
    v
Readable shared vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the important boundary: the server can identify a share, enforce expiry and revocation, and serve encrypted bytes. It cannot unlock the content because it never receives the passphrase-derived key.&lt;/p&gt;

&lt;p&gt;The actual frontend share creation code follows that model directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shareBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createEncryptedShareBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;exported_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;source_vault_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;include_attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includeAttachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, that bundle creation does three essential things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;derives a share key from the recipient passphrase using Argon2id&lt;/li&gt;
&lt;li&gt;encrypts the exported vault snapshot with AES-256-GCM&lt;/li&gt;
&lt;li&gt;returns only ciphertext plus &lt;code&gt;encryptionMeta&lt;/code&gt; needed for local unlock later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend then stores the encrypted blob and metadata like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;encryptedVaultApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createShare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vault&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.cmvshare`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;include_attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includeAttachments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;passphrase_hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passphraseHint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/vnd.cryptmind.share+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;size_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shareBundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;encryption_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shareBundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encryptionMeta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what is missing there: no plaintext tree, no raw title/notes payload, and no passphrase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attachments follow the same rule
&lt;/h3&gt;

&lt;p&gt;Attachments are where many “private” products quietly cheat.&lt;/p&gt;

&lt;p&gt;In MindMapVault, if a share includes files, the owner-side client first downloads the already encrypted attachment, decrypts it locally with the owner’s session key, and then immediately re-encrypts that plaintext with the share key before upload. That means the shared attachment is not the original vault attachment reused directly. It becomes a separate ciphertext bound to the share export.&lt;/p&gt;

&lt;p&gt;That extra step matters for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the recipient only needs the share passphrase, not the owner’s vault keys&lt;/li&gt;
&lt;li&gt;the backend still never needs access to attachment plaintext during sharing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the sharing model is not “backend mediates decryption.” It is “owner creates a new encrypted package for recipients.” That is a much safer design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Public link does not mean public plaintext
&lt;/h3&gt;

&lt;p&gt;The share URL itself is public in the same sense that any capability URL is public: if you know the identifier, you can ask the backend for the encrypted bundle. But what you get back is still ciphertext plus KDF metadata and optional hint text.&lt;/p&gt;

&lt;p&gt;The recipient page then performs local unlock in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;encryptedVaultApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downloadUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download_url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unlockedBundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;unlockEncryptedShareBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;passphrase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encryption_meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the trust boundary is explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend verifies that the share exists, is not revoked, and is not expired&lt;/li&gt;
&lt;li&gt;backend returns encrypted bytes&lt;/li&gt;
&lt;li&gt;browser derives the key and decrypts locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is how zero knowledge is preserved while still allowing practical sharing.&lt;/p&gt;

&lt;p&gt;The main point here is not novelty or cryptographic cleverness. Every primitive used is well‑understood. The value is in how responsibilities are split, keys are scoped, and plaintext is prevented from leaking into default backend paths.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>security</category>
      <category>productivity</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Privacy-first mind mapping app. Part 2: Rust Backend - Why the Pain Is Worth It</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Thu, 23 Apr 2026 15:39:13 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-2-rust-backend-why-the-pain-is-worth-it-3hni</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-2-rust-backend-why-the-pain-is-worth-it-3hni</guid>
      <description>&lt;p&gt;Choosing Rust for a backend can feel irrational at first.&lt;br&gt;
If your short‑term goal is raw development speed, Rust does not win. The compiler is unforgiving, type design takes real thought, and compile cycles can test your patience. On this project, there were phases where a full rebuild felt endless—and that kind of friction is emotionally expensive when all you want is to ship something that works.&lt;/p&gt;

&lt;p&gt;But I still chose Rust for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reliability under pressure&lt;/li&gt;
&lt;li&gt;predictable resource usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This backend handles encryption‑related workflows, file metadata, quota enforcement, and versioned storage paths. I wanted something that stays lean, stable, and boring under load. Rust makes that possible—after you push through the painful parts.&lt;br&gt;
Once the architecture settles, the payoff is real: a compact server binary that runs consistently and fails loudly when something is wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Simple Architecture, on Purpose
&lt;/h2&gt;

&lt;p&gt;The guiding rule for this backend: do not outsmart yourself.&lt;br&gt;
The overall structure is deliberately conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Axum router composition by feature&lt;/li&gt;
&lt;li&gt;Explicit state objects per route group&lt;/li&gt;
&lt;li&gt;Trait‑based storage abstractions&lt;/li&gt;
&lt;li&gt;Strict model separation between API and persistence layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Server startup is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.nest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;auth_sql_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth_state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.nest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/mindmaps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;mindmaps_sql_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mindmaps_state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.nest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;public_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;public_state&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each feature owns its routes, its state, and its responsibilities. There is no global god‑object, no implicit shared context, and no “we’ll refactor this later” escape hatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability‑Driven State, Not Concrete Dependencies
&lt;/h2&gt;

&lt;p&gt;Route handlers depend on what they need, not how it is implemented.&lt;br&gt;
Here is the state injected into mind‑map routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MindMapsSqlState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DynSqlStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;minio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MinioClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JwtService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;diagnostics_enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail here is not the fields themselves—it’s that persistence is expressed through traits. Handlers care about capabilities (load, update, store) rather than concrete database implementations.&lt;br&gt;
This is not about abstraction for abstraction’s sake. It keeps the surface area of each feature honest.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fetching a Mind Map: Ownership First, Then Data
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_mind_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MindMapsSqlState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AuthenticatedUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MindMapDetail&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_owned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;to_detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Nothing fancy here—and that is the point.&lt;br&gt;
Ownership checks happen before transformation. Models coming out of storage are never leaked directly to the API layer. Every conversion is explicit.&lt;br&gt;
Rust’s type system makes cutting corners uncomfortable, which is exactly what you want when dealing with encrypted artifacts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Key Distribution (Encryption Happens in the Browser)
&lt;/h2&gt;

&lt;p&gt;This system performs client‑side encryption. The backend never sees plaintext content.&lt;br&gt;
The backend’s job is to distribute encrypted key material safely and consistently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthSqlState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AuthenticatedUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyBundleResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
        &lt;span class="py"&gt;.db&lt;/span&gt;
        &lt;span class="nf"&gt;.load_user_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="nf"&gt;.ok_or_else&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="nn"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user not found"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyBundleResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;classical_public_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="py"&gt;.classical_public_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pq_public_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="py"&gt;.pq_public_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;classical_priv_encrypted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="py"&gt;.classical_priv_encrypted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;pq_priv_encrypted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="py"&gt;.pq_priv_encrypted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;key_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;db_user&lt;/span&gt;&lt;span class="py"&gt;.key_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every field in this response is intentionally named and versioned. There is no “future me will remember what this blob means.”&lt;br&gt;
Discipline here prevents silent cryptographic foot‑guns later.&lt;br&gt;
And upload a blob to S3 endpoint with authentication, quora check and versioning.&lt;/p&gt;
&lt;h2&gt;
  
  
  Uploading Encrypted Blobs with Quotas and Versioning
&lt;/h2&gt;

&lt;p&gt;Uploading a mind‑map snapshot involves more than just dumping bytes to object storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;upload_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MindMapsSqlState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AuthenticatedUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ConfirmUploadResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blob is required"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_owned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;subscription_tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_effective_subscription_tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;current_total_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_storage_usage_total_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.minio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;projected_total_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_total_bytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;plan_limit_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subscription_tier&lt;/span&gt;&lt;span class="nf"&gt;.storage_limit_bytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;projected_total_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;plan_limit_bytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_quota_exceeded_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;subscription_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;projected_total_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;plan_limit_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;version_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
        &lt;span class="py"&gt;.minio&lt;/span&gt;
        &lt;span class="nf"&gt;.upload_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.minio_object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;version_history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.version_history&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;version_history&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VersionSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;version_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version_id&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;eph_classical_public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.eph_classical_public&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;eph_pq_ciphertext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.eph_pq_ciphertext&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;wrapped_dek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.wrapped_dek&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;saved_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
        &lt;span class="py"&gt;.db&lt;/span&gt;
        &lt;span class="nf"&gt;.update_mind_map_upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;version_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version_history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleanup_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.minio&lt;/span&gt;&lt;span class="nf"&gt;.delete_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.minio_object_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;version_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;cleanup_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;map_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;version_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;version_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"failed to roll back uploaded version after metadata update error"&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;prune_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.minio_object_key&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;prune_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="py"&gt;.max_versions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;minio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.minio&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;minio&lt;/span&gt;&lt;span class="nf"&gt;.prune_versions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;prune_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prune_limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to prune old versions for {prune_key}: {e}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConfirmUploadResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;version_id&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_mind_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MindMapsSqlState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AuthenticatedUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MindMapDetail&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_owned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;to_detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the upload even happens, several constraints are enforced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request body must exist&lt;/li&gt;
&lt;li&gt;user must own the mind map&lt;/li&gt;
&lt;li&gt;storage quota must not be exceeded&lt;/li&gt;
&lt;li&gt;projected usage must include the new blob&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after all guards pass does the upload occur.&lt;br&gt;
Versioning metadata is recorded atomically with cleanup logic in case persistence fails. If the database update errors out, the uploaded object version is explicitly deleted. If that fails, the error is logged loudly.&lt;/p&gt;

&lt;p&gt;Finally, old versions are pruned asynchronously.&lt;/p&gt;

&lt;p&gt;This is not clever code. It is defensive code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling Helps—But Discipline Matters More
&lt;/h2&gt;

&lt;p&gt;rust-analyzer in VS Code can absolutely make your life easier. It can also overwhelm you with diagnostics if your mental model is fuzzy.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rdf6scjqap33f318evk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rdf6scjqap33f318evk.png" alt="rust-analzyer" width="800" height="502"&gt;&lt;/a&gt;&lt;br&gt;
The important part is discipline.&lt;br&gt;
Two patterns became essential in keeping this codebase sane:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;State Segregation per Route Area
Each domain owns its state:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;auth&lt;/li&gt;
&lt;li&gt;admin&lt;/li&gt;
&lt;li&gt;mind maps&lt;/li&gt;
&lt;li&gt;billing&lt;/li&gt;
&lt;li&gt;public endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No shared mutable mega‑state. No “just add it here for now.”&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Strongly Typed Data Contracts
Models like StoredMindMap, NewMindMap, and update‑specific structs force explicit mappings. Field drift becomes visible immediately instead of months later through corrupted data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Emotional Reality of Rust
&lt;/h2&gt;

&lt;p&gt;Developing in Rust is harder, at least compare to Python.&lt;br&gt;
Sometimes it feels like trying to speak while a grammar teacher interrupts every sentence. But once the code compiles and the model fits, the confidence is different.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer runtime surprises&lt;/li&gt;
&lt;li&gt;Clearer failure modes&lt;/li&gt;
&lt;li&gt;Stronger guarantees under refactoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why this “crazy” idea stayed.&lt;br&gt;
In the next part of this series, I’ll dive into how encryption boundaries shaped both the frontend and storage layout—and how Rust made those edges explicit rather than implicit&lt;/p&gt;

&lt;h3&gt;
  
  
  PS: A Note on Cognitive Load: Rust’s Memory Model Is Not Free
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;It would be dishonest to talk about Rust without calling out the extra cognitive complexity its memory model imposes on the developer.&lt;br&gt;
Ownership, borrowing, and lifetimes are not just compiler mechanics—they shape how you think. Every non‑trivial data flow forces you to reason about who owns what, for how long, and under which constraints. This is especially noticeable when designing API boundaries, async flows, or shared state. Even when the compiler eventually guides you to a correct solution, getting there can be mentally expensive.&lt;br&gt;
In practice, this means Rust demands more upfront thinking than garbage‑collected languages. The friction is real, and it slows you down early—sometimes significantly. There were moments in this project where the hardest part was not the business logic, but expressing it in a way that satisfied ownership rules without distorting the design.&lt;br&gt;
The trade‑off, however, is intentional pressure. Rust pushes complexity from runtime into design time. Once the code compiles, entire classes of memory bugs, data races, and lifetime errors simply stop being possible. The cognitive load doesn’t disappear—but it pays off by converting uncertainty into explicit structure.&lt;br&gt;
Rust does not make you faster by default. It makes you more precise, and precision has a cost.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For readers less familiar with Rust, some of this complexity shows up very visibly in the code itself: frequent symbols like &amp;amp;, *, ', &amp;lt; &amp;gt;, ::, and others. These are not decoration—they encode rules about ownership, borrowing, lifetimes, and type relationships. Even simple operations often carry extra markers that force the developer to be explicit about how memory is accessed and shared. This makes Rust code denser and harder to read at first glance for newcomers, but it is exactly this explicitness that allows the compiler to catch entire classes of errors before the program ever runs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For me personally, the most effective way to get over Rust’s initial hurdle was &lt;a href="https://www.linkedin.com/learning/rust-essential-training" rel="noopener noreferrer"&gt;Rust Essential Training&lt;/a&gt; by Barron Stone on LinkedIn Learning, which helped solidify ownership and borrowing concepts early on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>performance</category>
      <category>privacy</category>
      <category>rust</category>
    </item>
    <item>
      <title>Privacy-first mind mapping app. Part 1: Constraints Before Tech</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Wed, 22 Apr 2026 20:40:36 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-1-constraints-before-tech-50p3</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-1-constraints-before-tech-50p3</guid>
      <description>&lt;p&gt;I started with a romantic idea: build the entire product in Rust, with Rust‑first tools, end to end.&lt;br&gt;
I still like that idea philosophically. In practice, it was too much for one person and too far removed from the real goal.&lt;/p&gt;

&lt;p&gt;The real goal was simple and already described in &lt;a href="https://dev.to/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-0-motivations-and-mind-maps-4k0m"&gt;Part 0&lt;/a&gt;: a fast, practical, keyboard‑friendly app that is online and privacy‑respecting. Not a technology demo. Not a purity contest.&lt;br&gt;
At first, I treated the tech stack as if the technology choices were the product.&lt;/p&gt;

&lt;p&gt;For the backend, I settled on combo Rust with Tokio and Axum. I genuinely love FastAPI — it’s the framework I trust the most — but early in the project I wanted to keep operational costs low. Python frameworks tend to have a higher memory footprint, which isn’t ideal for a small, long‑running SaaS.&lt;br&gt;
On the desktop side, I chose Tauri. It allowed me to reuse the web frontend while keeping the application lightweight, secure, and fast. Rust on the backend and Tauri on the desktop ended up being a natural pairing.&lt;br&gt;
This was my final compromise — the last remaining piece of the original Rust‑first dream. Enough Rust to matter, without turning the project into an experiment.&lt;/p&gt;

&lt;p&gt;I tried RustFS as object storage. Under load, it quickly became clear that it wasn’t stable enough for my needs. Around the 200‑concurrent‑user mark, things started to fall apart. The project is still in alpha, so that’s no surprise. Maybe it’s a better fit for a future project.&lt;/p&gt;

&lt;p&gt;I spent a lot of time trying to make SurrealDB work smoothly in this setup, but I never reached a level of confidence that matched my timeline. Maybe it was the documentation, maybe a skill issue.&lt;br&gt;
MongoDB — a personal favorite — turned out not to be the right fit either, mostly due to its memory footprint. &lt;a href="https://stoolap.io/" rel="noopener noreferrer"&gt;Stoolap&lt;/a&gt; was explored and used in early phases, but maintaining multiple database experiments during an early‑stage product simply didn’t make sense.&lt;/p&gt;

&lt;p&gt;For the frontend, I experimented with Rust web UI approaches and Rust‑adjacent options. The biggest issue I ran into was the lack of a proper mind‑mapping canvas component. This canvas is the heart of the UI, and I didn’t feel confident enough to start developing something that complex from scratch. &lt;/p&gt;

&lt;p&gt;So I changed my approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MinIO for object storage — boring, but reliable&lt;/li&gt;
&lt;li&gt;Rust for the backend — the last practical remnant of my original Rust‑only dream&lt;/li&gt;
&lt;li&gt;PostgreSQL as the current backend storage baseline — a boring, proven, and hard to get wrong&lt;/li&gt;
&lt;li&gt;React + TypeScript for the frontend — chosen for iteration speed and mature tooling, with no need to reinvent anything&lt;/li&gt;
&lt;li&gt;Tauri for desktop apps — lightweight, secure, and a natural fit with Rust&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the beginning, my decisions were more ideological. Over time, I became pragmatic. I like things that get done, and there are always options to choose from.&lt;/p&gt;

&lt;p&gt;I wanted an offline‑first feeling, with encrypted persistence to the database and S3‑compatible storage. I wanted the app to feel objectively responsive and immediate. If a thought appears, I should be able to capture it in a fraction of a second — not after waiting for framework ceremonies.&lt;/p&gt;

&lt;p&gt;The key lesson of this chapter is simple: constraints come before tools.&lt;/p&gt;

&lt;p&gt;When your constraints are clear, technology becomes a means. When your constraints are blurry, technology becomes a trap. I lost time learning this lesson. I don’t regret the learning, but I wouldn’t repeat the same order again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mindmapvault.com/" rel="noopener noreferrer"&gt;MindMapVault&lt;/a&gt; exists because I finally prioritized the goal over the stack fantasy.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>productivity</category>
      <category>mindmap</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Privacy-first mind mapping app. Part 0: Motivations and Mind Maps</title>
      <dc:creator>Kornel Maraz</dc:creator>
      <pubDate>Mon, 20 Apr 2026 22:21:45 +0000</pubDate>
      <link>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-0-motivations-and-mind-maps-4k0m</link>
      <guid>https://forem.com/kornel_maraz_5e66a3e4e27d/privacy-first-mind-mapping-app-part-0-motivations-and-mind-maps-4k0m</guid>
      <description>&lt;p&gt;Before MindMapVault, I used FreeMind a lot.&lt;/p&gt;

&lt;p&gt;I loved it because it was fast, reliable, and easy to use. It respected keyboard-heavy work. I could stay focused on the idea itself instead of hunting for the right button, menu, or floating panel. I could move quickly, write down thoughts with typos if necessary, and keep going while the idea was still alive in my head.&lt;/p&gt;

&lt;p&gt;That mattered more than it may sound.&lt;/p&gt;

&lt;p&gt;With many modern applications, a surprising amount of attention gets wasted on the interface itself. You stop thinking about the problem and start thinking about the tool. You search for the correct icon. You look for the hidden command. You try to remember which mode you are in. That interruption is expensive. Sometimes the original thought is already weaker by the time you find what you needed.&lt;/p&gt;

&lt;p&gt;FreeMind was different for me.&lt;/p&gt;

&lt;p&gt;It felt simple in the best possible way. I did not need setup. I did not need to configure the whole universe before I could begin. I installed it and started working. That was it.&lt;/p&gt;

&lt;p&gt;I could use the workflow almost mechanically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;F3&lt;/code&gt; to edit a node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;F4&lt;/code&gt; for alternating colors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Insert&lt;/code&gt; for a new node right&lt;/li&gt;
&lt;li&gt;double &lt;code&gt;Enter&lt;/code&gt; new node underneath&lt;/li&gt;
&lt;li&gt;mostly keyboard, minimal friction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was very effective with that style of work. The application got out of the way.&lt;/p&gt;

&lt;p&gt;But it had one major weakness: it was an offline app.&lt;/p&gt;

&lt;p&gt;The map was always on the wrong computer when I needed it. It did not matter whether I tried to soften that with OneDrive sync or a similar workaround. The real problem remained the same. The tool I liked best was tied to one machine at the exact moment when I wanted access from somewhere else.&lt;/p&gt;

&lt;p&gt;What I wanted was not a giant reinvention.&lt;/p&gt;

&lt;p&gt;I wanted a quick and dirty web UI with the same spirit. Something fast. Something direct. Something that would let me capture the thought immediately and keep moving. I did not want a bloated collaboration platform pretending to help me think. I wanted the practical feeling I had with FreeMind, but available from wherever I was.&lt;/p&gt;

&lt;p&gt;I also wanted that accessibility without broadcasting my notes. In practice that meant an online tool where I remained in control of who sees my thoughts — convenient access without making personal notes public by default. Privacy here is about minimizing accidental exposure and keeping ownership and control over what I write, not about hiding anything questionable.&lt;/p&gt;

&lt;p&gt;I searched for that for a long time.&lt;/p&gt;

&lt;p&gt;I did not find what I wanted.&lt;/p&gt;

&lt;p&gt;That gap is one of the main reasons &lt;a href="https://www.mindmapvault.com/" rel="noopener noreferrer"&gt;MindMapVault&lt;/a&gt; exists.&lt;/p&gt;

&lt;p&gt;This blog series is an honest account of how I built it: the parts that worked, the parts that were painful, the mistakes, the redesigns, and the decisions that still feel right.&lt;/p&gt;

&lt;p&gt;If you have ever felt that older tools were somehow more efficient, more focused, and less eager to interrupt your thinking, you will probably understand the motivation behind this project.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>productivity</category>
      <category>devjournal</category>
      <category>mindmap</category>
    </item>
  </channel>
</rss>
