<?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>DEV Community: Fabien B.</title>
    <description>The latest articles on DEV Community by Fabien B. (@axfab).</description>
    <link>https://dev.to/axfab</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3943889%2F9fd2fb46-4f2a-4726-abe8-ec41d844e4e6.png</url>
      <title>DEV Community: Fabien B.</title>
      <link>https://dev.to/axfab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/axfab"/>
    <language>en</language>
    <item>
      <title>Software doesn't age. It stops adapting.</title>
      <dc:creator>Fabien B.</dc:creator>
      <pubDate>Sun, 26 Jul 2026 15:08:29 +0000</pubDate>
      <link>https://dev.to/axfab/software-doesnt-age-it-stops-adapting-13p3</link>
      <guid>https://dev.to/axfab/software-doesnt-age-it-stops-adapting-13p3</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep-dive into the mechanisms that make software progressively harder to evolve.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;An application written ten years ago still works.&lt;/p&gt;

&lt;p&gt;Users rely on it every day. The servers run. The tests pass. On the surface, nothing looks wrong.&lt;/p&gt;

&lt;p&gt;And yet, no one dares touch it anymore.&lt;/p&gt;

&lt;p&gt;Adding a feature that seemed simple takes several weeks. Fixing one bug reveals two more. Every change requires understanding mechanisms that no one on the team truly masters anymore. Developers end up working around the problem instead of modifying the code, afraid of breaking a balance that has become fragile.&lt;/p&gt;

&lt;p&gt;The software still works, but it seems to have lost something essential: its ability to evolve.&lt;/p&gt;

&lt;p&gt;We often talk about "legacy," technical debt, or obsolescence. These terms describe the symptoms well, but they don't answer a more fundamental question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can a piece of software that never changes become increasingly difficult to evolve?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After all, the source code is the same as yesterday. The algorithms haven't changed overnight. The files are still there, line for line.&lt;/p&gt;

&lt;p&gt;What has changed, on the other hand, is everything around this software.&lt;/p&gt;

&lt;p&gt;Business needs evolve. Technologies advance. Teams turn over. Tools change. Knowledge gets lost. User expectations rise. The ecosystem keeps moving forward while the application stays still.&lt;/p&gt;

&lt;p&gt;This might be the real paradox of software: &lt;strong&gt;it's not the code that ages, it's its fit with its environment that progressively degrades.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This distinction matters, because it changes how we approach modernization. If we think software ages simply because it's old, the only answer seems to be a rewrite. But if we understand that its real problem is an inability to adapt, a different question emerges:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we design software that can keep evolving, even as everything around it changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the idea I want to explore in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code doesn't age. Its environment does.
&lt;/h2&gt;

&lt;p&gt;Unlike a car that wears out from use, or food that degrades over time, source code is remarkably stable.&lt;/p&gt;

&lt;p&gt;An application left untouched in a drawer for ten years will be exactly the same when you reopen it. The files won't have changed a single line. The algorithms will still produce the same results. The original compiler will probably still be able to produce the same binary.&lt;/p&gt;

&lt;p&gt;In that sense, code doesn't age.&lt;/p&gt;

&lt;p&gt;And yet, we've all opened an old project with that strange feeling that it belongs to another era. Not because it no longer works, but because everything around it has changed.&lt;/p&gt;

&lt;p&gt;Operating systems evolve. Browsers change. Frameworks disappear. Libraries get security patches. Languages add new concepts. Business needs shift. Teams turn over. Engineering practices move forward.&lt;/p&gt;

&lt;p&gt;The software, meanwhile, stays frozen.&lt;/p&gt;

&lt;p&gt;It's precisely this gap that creates aging.&lt;/p&gt;

&lt;p&gt;Software doesn't become harder to maintain because it's old. It becomes harder to maintain because the gap between its original choices and its current environment keeps growing.&lt;/p&gt;

&lt;p&gt;This gap can take several forms. Some are purely technical. Others are organizational or human. Others still are directly tied to the business the software is meant to serve.&lt;/p&gt;

&lt;p&gt;Understanding these different forms of aging matters. Without it, we risk treating the symptoms — upgrading a framework, rewriting a technical layer, or switching languages — without ever addressing the real cause of the problem.&lt;/p&gt;

&lt;p&gt;In my view, software can age in six different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six forces that push software toward obsolescence
&lt;/h2&gt;

&lt;p&gt;We often talk about software aging as if it were a single, inevitable phenomenon — as if software became "old" simply because time passes.&lt;/p&gt;

&lt;p&gt;I don't think that's the case.&lt;/p&gt;

&lt;p&gt;Time isn't the real culprit. It's only a revealer.&lt;/p&gt;

&lt;p&gt;What ages software is the many forces acting on it over the years. Each one creates a gap between the assumptions made at design time and today's reality.&lt;/p&gt;

&lt;p&gt;The bigger this gap grows, the harder the software becomes to understand, maintain, and evolve.&lt;/p&gt;

&lt;p&gt;These forces aren't all technical. Some come from the business, others from the team, others still from the organization itself. This is actually what makes modernization so complex: upgrading a framework doesn't solve a business problem, and a new architecture doesn't replace lost knowledge.&lt;/p&gt;

&lt;p&gt;In my view, there are six major forces that progressively push software toward obsolescence.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Technical pressure
&lt;/h3&gt;

&lt;p&gt;Languages evolve.&lt;/p&gt;

&lt;p&gt;Frameworks change.&lt;/p&gt;

&lt;p&gt;Libraries patch security flaws.&lt;/p&gt;

&lt;p&gt;Operating systems, browsers, and platforms keep evolving.&lt;/p&gt;

&lt;p&gt;Software that is never maintained inevitably drifts away from its ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Business pressure
&lt;/h3&gt;

&lt;p&gt;Software was designed to meet a company's needs... at a given point in time.&lt;/p&gt;

&lt;p&gt;But a company evolves.&lt;/p&gt;

&lt;p&gt;Its processes change, so do its products, and so do its customers' expectations.&lt;/p&gt;

&lt;p&gt;When the software's business model stops evolving at the same pace as the company's, every new feature becomes an exercise in contortion.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Architectural pressure
&lt;/h3&gt;

&lt;p&gt;An architecture is always an answer to a context.&lt;/p&gt;

&lt;p&gt;An application designed for five developers may no longer fit a team of fifty.&lt;/p&gt;

&lt;p&gt;Conversely, some applications stay surprisingly simple even though they inherited an architecture built for needs that never actually materialized.&lt;/p&gt;

&lt;p&gt;A good architecture isn't universal; it's suited to a given situation. When that situation changes, the architecture can become a constraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Human pressure
&lt;/h3&gt;

&lt;p&gt;Teams change.&lt;/p&gt;

&lt;p&gt;Developers leave.&lt;/p&gt;

&lt;p&gt;Tacit knowledge disappears.&lt;/p&gt;

&lt;p&gt;New hires never lived through the decisions that shaped the software.&lt;/p&gt;

&lt;p&gt;A system that no one truly understands always ends up hard to evolve, even if its code is high quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Organizational pressure
&lt;/h3&gt;

&lt;p&gt;The company grows.&lt;/p&gt;

&lt;p&gt;Responsibilities shift.&lt;/p&gt;

&lt;p&gt;Teams reorganize.&lt;/p&gt;

&lt;p&gt;New regulatory constraints appear.&lt;/p&gt;

&lt;p&gt;The software now has to meet needs that didn't exist when it was designed.&lt;/p&gt;

&lt;p&gt;An architecture that worked perfectly for a small team can become a bottleneck in a much larger organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The pressure of ideas
&lt;/h3&gt;

&lt;p&gt;This one is probably the quietest, but also one of the most important.&lt;/p&gt;

&lt;p&gt;The way we build software keeps changing.&lt;/p&gt;

&lt;p&gt;New practices emerge. Some disappear. Others become standards.&lt;/p&gt;

&lt;p&gt;Developers joining a project today no longer share the same references as those who designed it fifteen years ago.&lt;/p&gt;

&lt;p&gt;It's not just the technology that changes: it's also how we think about software.&lt;/p&gt;

&lt;p&gt;A project built on concepts that have become rare or forgotten becomes progressively harder to understand — not because it's bad, but because the collective culture has moved on.&lt;/p&gt;




&lt;p&gt;These six forces are always at work.&lt;/p&gt;

&lt;p&gt;None of them alone is responsible for software aging.&lt;/p&gt;

&lt;p&gt;It's their accumulation that, year after year, gradually pulls an application away from its environment.&lt;/p&gt;

&lt;p&gt;And that's precisely why modernization can't be reduced to a simple technical upgrade. It's fundamentally about closing that gap so the software regains its ability to evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aging or technical debt?
&lt;/h2&gt;

&lt;p&gt;At this point, one question often comes up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't this simply technical debt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I don't think so.&lt;/p&gt;

&lt;p&gt;Technical debt is an extremely useful concept, but it's often used to describe every problem found in an existing application. Over time, it becomes a universal explanation that no longer really helps us understand what's happening.&lt;/p&gt;

&lt;p&gt;To me, software aging is a broader phenomenon.&lt;/p&gt;

&lt;p&gt;Technical debt is one of its possible consequences, but it's neither its only cause nor its only manifestation.&lt;/p&gt;

&lt;p&gt;Take an example.&lt;/p&gt;

&lt;p&gt;An application built fifteen years ago may have been designed with great care. Clear code, well organized, tests in place, technical choices perfectly suited to the time.&lt;/p&gt;

&lt;p&gt;Yet fifteen years later, it may run on an abandoned framework, serve a business model that no longer matches the company's activity, or rely on concepts that new developers no longer encounter anywhere else.&lt;/p&gt;

&lt;p&gt;This application has aged.&lt;/p&gt;

&lt;p&gt;But it would be hard to say it carries technical debt in the traditional sense.&lt;/p&gt;

&lt;p&gt;Conversely, an application built only a year ago can already carry considerable technical debt.&lt;/p&gt;

&lt;p&gt;Premature abstractions.&lt;/p&gt;

&lt;p&gt;Circular dependencies.&lt;/p&gt;

&lt;p&gt;Useless layers.&lt;/p&gt;

&lt;p&gt;An architecture copied without reflection.&lt;/p&gt;

&lt;p&gt;Contradictory conventions.&lt;/p&gt;

&lt;p&gt;The software is recent.&lt;/p&gt;

&lt;p&gt;Yet every new feature is already harder to integrate.&lt;/p&gt;

&lt;p&gt;It hasn't had time to age.&lt;/p&gt;

&lt;p&gt;It has, however, accumulated debt.&lt;/p&gt;

&lt;p&gt;That's why I prefer to keep these two notions separate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aging&lt;/strong&gt; describes the gap that progressively widens between software and its environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical debt&lt;/strong&gt; refers to technical choices whose cost has been deferred to the future.&lt;/p&gt;

&lt;p&gt;The two phenomena often reinforce each other.&lt;/p&gt;

&lt;p&gt;Significant technical debt accelerates aging, because it makes updates, refactoring, and changes more costly.&lt;/p&gt;

&lt;p&gt;Conversely, software that ages without maintenance almost always ends up accumulating new debt: workarounds, temporary fixes, code duplication, or successive patches.&lt;/p&gt;

&lt;p&gt;But conflating the two often leads to bad decisions.&lt;/p&gt;

&lt;p&gt;If we think everything is technical debt, the answer is usually to "clean up the code."&lt;/p&gt;

&lt;p&gt;Yet sometimes the real problem isn't the code itself — it's that the application no longer fits the context it operates in.&lt;/p&gt;

&lt;p&gt;In that case, modernization isn't just about improving code quality.&lt;/p&gt;

&lt;p&gt;It's mainly about closing the gap between the software and the world around it, so it regains its ability to evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aging or maturity?
&lt;/h2&gt;

&lt;p&gt;At this point, one might think that old software is necessarily software nearing the end of its life.&lt;/p&gt;

&lt;p&gt;That would be a mistake.&lt;/p&gt;

&lt;p&gt;The age of a piece of software ultimately tells us very little about its health.&lt;/p&gt;

&lt;p&gt;Some decades-old projects remain references. They keep evolving, keep adding features, adapt to new use cases, and remain pleasant to maintain.&lt;/p&gt;

&lt;p&gt;Conversely, some applications built only a few years ago are already considered "legacy" systems.&lt;/p&gt;

&lt;p&gt;The difference isn't their age.&lt;/p&gt;

&lt;p&gt;It's their ability to evolve.&lt;/p&gt;

&lt;p&gt;I like to draw a distinction between software &lt;strong&gt;that ages&lt;/strong&gt; and software &lt;strong&gt;that matures&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Software that ages accumulates gaps with its environment. Every change becomes harder than the last. Yesterday's decisions progressively become constraints. The team spends more and more energy working around the system's limitations instead of addressing business needs.&lt;/p&gt;

&lt;p&gt;Mature software, on the other hand, keeps evolving alongside its environment.&lt;/p&gt;

&lt;p&gt;Its architecture isn't frozen. It's regularly adapted. Dependencies are kept up to date. Concepts get questioned when the context changes. The code is simplified as much as it's enriched.&lt;/p&gt;

&lt;p&gt;In other words, mature software isn't software that no longer changes.&lt;/p&gt;

&lt;p&gt;It's software that has learned how to change.&lt;/p&gt;

&lt;p&gt;This distinction matters, because it changes how we evaluate a system.&lt;/p&gt;

&lt;p&gt;Age isn't a relevant indicator.&lt;/p&gt;

&lt;p&gt;What matters is how fast the software can evolve without degrading.&lt;/p&gt;

&lt;p&gt;Can you add a feature without triggering a cascade of regressions?&lt;/p&gt;

&lt;p&gt;Can you upgrade a dependency without freezing the team for several weeks?&lt;/p&gt;

&lt;p&gt;Do new developers quickly understand how the system works?&lt;/p&gt;

&lt;p&gt;Do the architectural choices still match today's needs?&lt;/p&gt;

&lt;p&gt;These questions say far more about a system's health than its creation date.&lt;/p&gt;

&lt;p&gt;That's probably why some projects go through several decades without ever becoming a burden, while others already feel like they belong to the past after just a few years.&lt;/p&gt;

&lt;p&gt;Maturity isn't the absence of change.&lt;/p&gt;

&lt;p&gt;It's the ability to absorb change without losing coherence.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to slow down software aging
&lt;/h2&gt;

&lt;p&gt;If software aging is inevitable, that doesn't mean we're condemned to just endure it.&lt;/p&gt;

&lt;p&gt;Just as it's impossible to stop a building from wearing down, it's impossible to freeze software in some ideal state. What is entirely possible, however, is to slow its aging and preserve its ability to evolve.&lt;/p&gt;

&lt;p&gt;This takes less in the way of technical recipes than discipline and self-questioning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accept that software is never finished
&lt;/h3&gt;

&lt;p&gt;One of the most common mistakes is treating a production release as the end of the project.&lt;/p&gt;

&lt;p&gt;In reality, it's only the beginning.&lt;/p&gt;

&lt;p&gt;Once people start using the software, they discover new needs. They suggest improvements. They question certain decisions. Sometimes they use the application in ways no one ever imagined.&lt;/p&gt;

&lt;p&gt;Sponsors often follow the same path. A first version answers the initial need, but once the first benefits show up, new ideas naturally appear.&lt;/p&gt;

&lt;p&gt;This is especially visible when software automates part of the work. The time saved lets teams think about other improvements, imagine new processes or new services. Effective software often creates its own follow-on demand.&lt;/p&gt;

&lt;p&gt;On top of that, there's market change, regulatory constraints, organizational shifts, or new user expectations.&lt;/p&gt;

&lt;p&gt;Software that delivers value is software people will always ask more of.&lt;/p&gt;

&lt;p&gt;In the end, good software is a bit like a competent employee: the more effective it is, the more responsibility it gets handed.&lt;/p&gt;

&lt;p&gt;Designing software isn't about building a finished product. It's about building a system able to absorb changes you don't know about yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prepare the ground before adding a new feature
&lt;/h3&gt;

&lt;p&gt;When a new need arises, it's tempting to reach for the fastest possible solution.&lt;/p&gt;

&lt;p&gt;After all, you just have to "add a feature."&lt;/p&gt;

&lt;p&gt;But that view is often misleading.&lt;/p&gt;

&lt;p&gt;Every new feature shifts the system's balance. It interacts with existing behaviors, introduces new rules, and increases the number of cases to account for.&lt;/p&gt;

&lt;p&gt;Stack enough of these changes without ever revisiting what already exists, and the software ends up looking like a game of Jenga. Each new piece seems to hold, but every addition makes the whole thing a little more fragile.&lt;/p&gt;

&lt;p&gt;Sometimes it's worth spending a bit of time preparing the ground before starting development.&lt;/p&gt;

&lt;p&gt;That doesn't mean rewriting the application or launching a massive refactoring effort.&lt;/p&gt;

&lt;p&gt;It means adapting the foundation so the new feature naturally fits into the existing architecture.&lt;/p&gt;

&lt;p&gt;Good development isn't just about making a new feature work. It's also about preserving the coherence of the whole.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve simplicity
&lt;/h3&gt;

&lt;p&gt;Every new feature increases a piece of software's complexity.&lt;/p&gt;

&lt;p&gt;So the question isn't whether to add complexity, but how to keep it from piling up faster than the value it produces.&lt;/p&gt;

&lt;p&gt;Every extra layer is one more contract to maintain.&lt;/p&gt;

&lt;p&gt;Every dependency becomes an added responsibility.&lt;/p&gt;

&lt;p&gt;Every abstraction will eventually need to be understood, explained, and probably adapted.&lt;/p&gt;

&lt;p&gt;Simplicity isn't a pursuit of elegance.&lt;/p&gt;

&lt;p&gt;It's a strategy.&lt;/p&gt;

&lt;p&gt;A simple system is easier to understand, easier to change, and easier to reorganize when the context shifts.&lt;/p&gt;

&lt;p&gt;Conversely, an unnecessarily complex system progressively becomes rigid. Developers hesitate to touch the code, changes take longer, and every change feels riskier than the last.&lt;/p&gt;

&lt;p&gt;Preserving simplicity isn't about writing as little code as possible.&lt;/p&gt;

&lt;p&gt;It's about keeping only the complexity that's actually necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regularly question past decisions
&lt;/h3&gt;

&lt;p&gt;The best technical decisions are always made within a given context.&lt;/p&gt;

&lt;p&gt;An architecture suited to a small team won't necessarily still make sense once the project has dozens of developers.&lt;/p&gt;

&lt;p&gt;A data model suited to a few thousand users may no longer serve the needs of several million.&lt;/p&gt;

&lt;p&gt;Business needs change.&lt;/p&gt;

&lt;p&gt;Technologies evolve.&lt;/p&gt;

&lt;p&gt;Regulatory constraints appear.&lt;/p&gt;

&lt;p&gt;The company itself transforms.&lt;/p&gt;

&lt;p&gt;Given all that, it would be surprising if every decision made five or ten years ago were still the best one today.&lt;/p&gt;

&lt;p&gt;Questioning a past decision doesn't mean it was wrong.&lt;/p&gt;

&lt;p&gt;It simply means the context has changed.&lt;/p&gt;

&lt;p&gt;A good architecture isn't one that resists change.&lt;/p&gt;

&lt;p&gt;It's one that accepts being questioned when change becomes necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remove as much as you add
&lt;/h3&gt;

&lt;p&gt;Development usually means adding.&lt;/p&gt;

&lt;p&gt;Adding a feature.&lt;/p&gt;

&lt;p&gt;Adding a business rule.&lt;/p&gt;

&lt;p&gt;Adding a class.&lt;/p&gt;

&lt;p&gt;Adding a service.&lt;/p&gt;

&lt;p&gt;Yet the best software also evolves because it knows how to remove things.&lt;/p&gt;

&lt;p&gt;New features sometimes make parts of the system unnecessary. Two mechanisms end up doing the same thing. An old business rule no longer has a reason to exist. An abstraction created a few years ago no longer serves any purpose today.&lt;/p&gt;

&lt;p&gt;If nothing is ever removed, the software keeps growing without ever getting simpler.&lt;/p&gt;

&lt;p&gt;Execution paths multiply.&lt;/p&gt;

&lt;p&gt;Exceptions pile up.&lt;/p&gt;

&lt;p&gt;Duplicates appear.&lt;/p&gt;

&lt;p&gt;The system becomes harder to understand — not because it does more, but because it keeps every trace of its history.&lt;/p&gt;

&lt;p&gt;Every new change should be an opportunity to ask not just what needs to be added, but also what can disappear.&lt;/p&gt;

&lt;p&gt;Removing code is often one of the best ways to slow down an application's aging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document the decisions, not the code
&lt;/h3&gt;

&lt;p&gt;We often hear that good code should be clear enough to not need documentation.&lt;/p&gt;

&lt;p&gt;I agree — up to a point.&lt;/p&gt;

&lt;p&gt;Well-written code usually explains &lt;strong&gt;how&lt;/strong&gt; the software works.&lt;/p&gt;

&lt;p&gt;It rarely explains &lt;strong&gt;why&lt;/strong&gt; it works that way.&lt;/p&gt;

&lt;p&gt;Why this architecture?&lt;/p&gt;

&lt;p&gt;Why this naming convention?&lt;/p&gt;

&lt;p&gt;Why this business vocabulary?&lt;/p&gt;

&lt;p&gt;Why this constraint?&lt;/p&gt;

&lt;p&gt;Why this dependency rather than another?&lt;/p&gt;

&lt;p&gt;These decisions aren't visible in the code.&lt;/p&gt;

&lt;p&gt;Yet they deserve to be preserved, because they're what let future developers understand the choices that were made.&lt;/p&gt;

&lt;p&gt;Good documentation doesn't try to describe every method or every class.&lt;/p&gt;

&lt;p&gt;It captures the project's most stable elements: business concepts, team conventions, architectural decisions, and their rationale.&lt;/p&gt;

&lt;p&gt;And above all, it has to live alongside the project.&lt;/p&gt;

&lt;p&gt;Documentation versioned with the code stands a real chance of evolving with it.&lt;/p&gt;

&lt;p&gt;A hundred-and-twenty-page Word document buried on SharePoint, on the other hand, almost always ends up as an archive no one ever opens again.&lt;/p&gt;




&lt;p&gt;Slowing software aging isn't about preventing change.&lt;/p&gt;

&lt;p&gt;It's exactly the opposite.&lt;/p&gt;

&lt;p&gt;You have to accept that change is permanent, and design the software so it can absorb that change without losing coherence.&lt;/p&gt;

&lt;p&gt;Sometimes that means investing a bit more time today to save a lot of time tomorrow.&lt;/p&gt;

&lt;p&gt;Because the real cost of a feature isn't the cost of building it.&lt;/p&gt;

&lt;p&gt;It's the cost it will impose on every future change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We often talk about "old" software, "legacy" systems, or technical debt, as if time were the main culprit behind their condition.&lt;/p&gt;

&lt;p&gt;Yet time does nothing on its own.&lt;/p&gt;

&lt;p&gt;It doesn't change the code. It doesn't add complexity. It doesn't create dependencies. It doesn't erase knowledge.&lt;/p&gt;

&lt;p&gt;It's the changes happening all around it that progressively create a gap between the software and its environment.&lt;/p&gt;

&lt;p&gt;It's this gap that makes changes more expensive, decisions harder, and teams more hesitant.&lt;/p&gt;

&lt;p&gt;Software that keeps adapting, on the other hand, can stay relevant for decades. Its architecture evolves, its code gets simpler, its concepts get questioned when necessary. It doesn't try to resist change; it learns to live with it.&lt;/p&gt;

&lt;p&gt;This might be the real role of software engineering.&lt;/p&gt;

&lt;p&gt;We're not building software meant to stay still.&lt;/p&gt;

&lt;p&gt;We're building software meant to evolve.&lt;/p&gt;

&lt;p&gt;And if I had to sum up this article in a single sentence, it would be this one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Software doesn't age. It stops adapting.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else — architecture, refactoring, simplicity, documentation, even technical debt — ultimately serves a single purpose:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preserving, year after year, a piece of software's ability to change.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Build an Electron app with local data storage — no SQLite bindings, no server</title>
      <dc:creator>Fabien B.</dc:creator>
      <pubDate>Wed, 10 Jun 2026 22:36:47 +0000</pubDate>
      <link>https://dev.to/axfab/build-an-electron-app-with-local-data-storage-no-sqlite-bindings-no-server-45b3</link>
      <guid>https://dev.to/axfab/build-an-electron-app-with-local-data-storage-no-sqlite-bindings-no-server-45b3</guid>
      <description>&lt;p&gt;If you've shipped an Electron app that persists structured data, you've probably hit the wall at some point. You need something more than a flat JSON file, but a full database server is absurd for a desktop app. So you reach for better-sqlite3 — and then spend the next hour fighting native bindings.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Electron storage problem
&lt;/h2&gt;

&lt;p&gt;Electron bundles its own version of Node.js. That version almost never matches the one on your machine. Native Node modules — the ones that compile C++ bindings — need to be recompiled specifically for the Electron runtime, using &lt;code&gt;electron-rebuild&lt;/code&gt; or equivalent tooling.&lt;/p&gt;

&lt;p&gt;In practice this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every Electron update potentially breaks your native dependencies&lt;/li&gt;
&lt;li&gt;CI pipelines need platform-specific build steps for Windows, macOS and Linux&lt;/li&gt;
&lt;li&gt;Code signing and notarization on macOS gets complicated by native binaries&lt;/li&gt;
&lt;li&gt;End users on some machines encounter install failures you can't reproduce locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;better-sqlite3 is an excellent library. But it is a native module, and native modules in Electron are a tax you pay forever.&lt;/p&gt;

&lt;p&gt;The alternatives people reach for have their own problems. lowdb and lokijs keep everything in memory and flush to disk — fine for small datasets, but you lose durability on crash and memory usage grows with your data. nedb hasn't been maintained in years. A remote database is obviously wrong for a local desktop app.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you actually need
&lt;/h2&gt;

&lt;p&gt;For most Electron apps, the storage requirements are modest but specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured documents, not just key-value pairs&lt;/li&gt;
&lt;li&gt;Queries that go beyond "give me everything"&lt;/li&gt;
&lt;li&gt;Writes that survive a crash&lt;/li&gt;
&lt;li&gt;Zero native binaries — pure JavaScript, works everywhere Electron runs&lt;/li&gt;
&lt;li&gt;No server process running alongside your app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's exactly the gap pocket-db is designed to fill. It's a single-file embedded document store — the SQLite model applied to JSON documents, with a MongoDB-style API. Pure TypeScript, shipped as both ESM and CommonJS. No native bindings. No optional dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the example app
&lt;/h2&gt;

&lt;p&gt;Let's build a small note-taking app: create, list, search and delete notes. Simple enough to follow in full, realistic enough to show the pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;We'll use &lt;a href="https://electron-vite.org" rel="noopener noreferrer"&gt;electron-vite&lt;/a&gt;, which scaffolds the &lt;code&gt;src/main&lt;/code&gt; / &lt;code&gt;src/preload&lt;/code&gt; / &lt;code&gt;src/renderer&lt;/code&gt; layout used below, with TypeScript and React ready to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create @quick-start/electron@latest my-notes &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; react-ts
&lt;span class="nb"&gt;cd &lt;/span&gt;my-notes
npm &lt;span class="nb"&gt;install&lt;/span&gt; @axfab/pocket-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;electron-rebuild&lt;/code&gt;. No postinstall scripts. That's the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialising the database
&lt;/h3&gt;

&lt;p&gt;In Electron, database access belongs in the main process. You have access to the filesystem there, and it keeps your renderer process clean. Use &lt;code&gt;app.getPath('userData')&lt;/code&gt; — Electron's standard location for app data, persisted across updates.&lt;/p&gt;

&lt;p&gt;One thing to know: Electron creates the &lt;code&gt;userData&lt;/code&gt; directory lazily, so on a first launch it may not exist yet — and &lt;code&gt;open()&lt;/code&gt; does not create parent directories. Create it before opening:&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="c1"&gt;// src/main/db.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@axfab/pocket-db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userData&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// may not exist on first launch&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes.pdb&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file, one call. The database is created if it doesn't exist. No migrations, no schema definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposing database operations via IPC
&lt;/h3&gt;

&lt;p&gt;Electron's security model means the renderer can't touch Node APIs directly. You expose operations through IPC handlers in the main process and call them via &lt;code&gt;contextBridge&lt;/code&gt; in the preload script.&lt;/p&gt;

&lt;p&gt;Search deserves a moment of care: the search term comes from user input, and &lt;code&gt;$regex&lt;/code&gt; compiles it as a regular expression. A term containing &lt;code&gt;(&lt;/code&gt; or &lt;code&gt;[&lt;/code&gt; would throw, and a raw pattern is case-sensitive. Escape the input and pass &lt;code&gt;$options: 'i'&lt;/code&gt;:&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="c1"&gt;// src/main/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ipcMain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="cm"&gt;/** Escapes regex metacharacters so user input is matched literally. */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escapeRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.*+?^${}()|[&lt;/span&gt;&lt;span class="se"&gt;\]\\]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:create&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;span class="nx"&gt;_event&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;return&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:list&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;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:search&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;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escapeRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;$or&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&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;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$regex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;i&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;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:delete&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;span class="nx"&gt;_event&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;return&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteOne&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;before-quit&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;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/preload/index.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Note: keep the preload compiled to CommonJS (electron-vite's default) —&lt;/span&gt;
&lt;span class="c1"&gt;// sandboxed renderers don't load ESM preload scripts.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exposeInMainWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes&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;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:create&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes:delete&lt;/span&gt;&lt;span class="dl"&gt;'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using it in the renderer
&lt;/h3&gt;

&lt;p&gt;From the renderer, the API is just async function calls:&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="c1"&gt;// src/renderer/src/App.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleCreate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&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;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;setNotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;())&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;handleSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;setNotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&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;handleDelete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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="nf"&gt;setNotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&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;No database logic in the renderer. No connection strings. No async initialisation to wait for. The main process owns the database, the renderer just calls handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where indexes help — and where they don't
&lt;/h3&gt;

&lt;p&gt;Two things to know about pocket-db's query planner in V1: &lt;code&gt;$regex&lt;/code&gt; is never index-assisted (it's always evaluated against the documents), and &lt;code&gt;$or&lt;/code&gt; queries bypass index planning entirely. So the regex search above is a full collection scan — perfectly fine for a notes app, but don't expect an index to speed it up.&lt;/p&gt;

&lt;p&gt;Indexes shine on equality and range lookups. Say notes belong to notebooks:&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="c1"&gt;// run once at startup, safe to call repeatedly&lt;/span&gt;
&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notebook&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;span class="na"&gt;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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a query like &lt;code&gt;notes.find({ notebook: 'work' })&lt;/code&gt; reads only the matching documents instead of scanning the collection — the planner picks the index up automatically, no change needed in your query code. The same goes for a number index on &lt;code&gt;createdAt&lt;/code&gt; if you query by date range.&lt;/p&gt;

&lt;h3&gt;
  
  
  Periodic compaction
&lt;/h3&gt;

&lt;p&gt;The append-only log accumulates dead records as notes are edited and deleted. Schedule compaction when the app is idle:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser-window-blur&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;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compact&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;&lt;code&gt;compact()&lt;/code&gt; is fast and synchronous — it rewrites the file in a single forward pass. Calling it when the window loses focus means it runs during natural pauses without affecting responsiveness.&lt;/p&gt;




&lt;h2&gt;
  
  
  Packaging and distribution
&lt;/h2&gt;

&lt;p&gt;This is where the native module problem usually surfaces. With pocket-db, there's nothing to rebuild. The electron-vite scaffold already wires up electron-builder; its config just works as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# electron-builder.yml (generated by the scaffold)&lt;/span&gt;
&lt;span class="na"&gt;mac&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dmg&lt;/span&gt;
&lt;span class="na"&gt;win&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nsis&lt;/span&gt;
&lt;span class="na"&gt;linux&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AppImage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;electron-rebuild&lt;/code&gt; in your build pipeline. No platform-specific post-install hooks. The same config builds cleanly on all three platforms.&lt;/p&gt;

&lt;p&gt;If you're using GitHub Actions to build releases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/release.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build &amp;amp;&amp;amp; npx electron-builder&lt;/span&gt;
  &lt;span class="c1"&gt;# No electron-rebuild step needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One fewer thing to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about data migration?
&lt;/h2&gt;

&lt;p&gt;pocket-db is schemaless — documents in the same collection can have different shapes. That means additive changes (adding a new field) require nothing: new documents have the field, old ones don't, and &lt;code&gt;$exists&lt;/code&gt; lets you query for either.&lt;/p&gt;

&lt;p&gt;For breaking changes, a one-time migration on startup is straightforward. Keep a version marker in a metadata collection — and remember that on a fresh database the marker doesn't exist yet:&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_meta&lt;/span&gt;&lt;span class="dl"&gt;'&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;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;version&lt;/span&gt;&lt;span class="dl"&gt;'&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;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// run migration&lt;/span&gt;
  &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMany&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;archived&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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;Note the &lt;code&gt;replaceOne(marker._id, …)&lt;/code&gt; call: the first argument is the document &lt;strong&gt;id&lt;/strong&gt;. (Passing a full document as the only argument is a separate overload that replaces that document by itself — not what you want here.)&lt;/p&gt;

&lt;p&gt;No migration framework needed for most desktop app scenarios.&lt;/p&gt;




&lt;h2&gt;
  
  
  The trade-offs to know
&lt;/h2&gt;

&lt;p&gt;pocket-db is designed for single-process use. An Electron app fits that model perfectly — the main process is the only writer, and that's exactly the intended use case.&lt;/p&gt;

&lt;p&gt;It is not the right choice if your Electron app spawns multiple Node worker processes that all need to write to the same database simultaneously. It is also not the right choice if you're storing hundreds of thousands of documents and need complex aggregation — at that scale, an actual database server starts making sense even in a desktop context.&lt;/p&gt;

&lt;p&gt;For the common case — a desktop app storing user data, preferences, history, cached API responses, local task lists — it fits well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @axfab/pocket-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation at &lt;a href="https://pocket-db.axfab.net" rel="noopener noreferrer"&gt;pocket-db.axfab.net&lt;/a&gt;. Source and issues at &lt;a href="https://github.com/axfab/pocket-db" rel="noopener noreferrer"&gt;github.com/axfab/pocket-db&lt;/a&gt;. The complete example app from this article is in the &lt;a href="https://github.com/AxFab/pocket-example/tree/develop/01_electron" rel="noopener noreferrer"&gt;repository of exemple&lt;/a&gt;, tests included.&lt;/p&gt;

&lt;p&gt;If you've been putting up with native binding headaches in Electron, give it a try and let me know how it goes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;pocket-db is MIT licensed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>electron</category>
      <category>node</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>pocket-db vs lowdb vs LokiJS: an honest embedded database benchmark</title>
      <dc:creator>Fabien B.</dc:creator>
      <pubDate>Sun, 31 May 2026 18:02:52 +0000</pubDate>
      <link>https://dev.to/axfab/pocket-db-vs-lowdb-vs-lokijs-an-honest-embedded-database-benchmark-1bjg</link>
      <guid>https://dev.to/axfab/pocket-db-vs-lowdb-vs-lokijs-an-honest-embedded-database-benchmark-1bjg</guid>
      <description>&lt;p&gt;If you've ever built a CLI tool, an Electron app, or a local-first prototype in Node.js, you've faced the same question: &lt;em&gt;where do I store structured data without spinning up a server?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The usual suspects are well known — lowdb, LokiJS, nedb, better-sqlite3. I recently built &lt;strong&gt;pocket-db&lt;/strong&gt;, a new embedded document store for Node.js.&lt;/p&gt;

&lt;p&gt;I wanted a database solution that offered me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single-file database&lt;/li&gt;
&lt;li&gt;no native compilation&lt;/li&gt;
&lt;li&gt;Mongo-like API&lt;/li&gt;
&lt;li&gt;easy embedding in CLI or Electron apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to know honestly where it stands. So I ran a benchmark across 10 common operations on a collection of 1,000 documents.&lt;br&gt;
Here's what I found — not shying away from the unflattering parts.&lt;/p&gt;


&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pocket-db&lt;/strong&gt; — append-only single-file document store, MongoDB-like API, indexes in memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sqlite (in-memory)&lt;/strong&gt; — better-sqlite3 with an in-memory database, the fastest SQLite configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sqlite (file)&lt;/strong&gt; — better-sqlite3 persisted to disk, the most common SQLite setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;json-file&lt;/strong&gt; — full JSON file read/write on every operation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lowdb&lt;/strong&gt; — JSON-based, keeps everything in memory and flushes to disk on write&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lokijs&lt;/strong&gt; — in-memory document store, optional persistence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All tests run on Apple M-series hardware. Results are in &lt;strong&gt;ops/sec&lt;/strong&gt; — so higher is better.&lt;/p&gt;


&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operation                  pocket-db  sqlite (mem)  sqlite (file)  json-file      lowdb     lokijs
─────────────────────────────────────────────────────────────────────────────────────────────────
insertOne                    198,177     226,278 *         4,421      3,893      2,256      1,004
insertMany (100)               2,345       2,963 *           402        788        628        264
findById                     142,776   1,064,774         248,942  12,532,585 * 321,548  4,061,606
findAll                           96         361             361    115,774    870,822 *     1,179
findByName (scan)                 97         536             524     19,579     24,235 *     8,222
findByRole (index)               277         884             887     18,527     21,796 *     3,215
updateOne                     97,889     423,072 *         2,675        784        566        188
deleteOne                    454,402     465,026 *         5,551        924        663        220
countAll                      12,038   2,233,389         285,285  42,553,191 * 34,914,251 37,348,273
sortByScore (desc)                91         293             293      4,947      5,099 *     1,123
─────────────────────────────────────────────────────────────────────────────────────────────────
All values in ops/sec. * = fastest for this operation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Before you ask: json-file wins read benchmarks because the entire dataset is already loaded in memory during the benchmark.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why pocket-db writes so fast
&lt;/h2&gt;

&lt;p&gt;The short answer: every write is a single sequential append to the end of the file. No B-tree rebalancing. No page allocation. No full-file reserialisation.&lt;/p&gt;

&lt;p&gt;When you call &lt;code&gt;insertOne&lt;/code&gt;, &lt;code&gt;updateOne&lt;/code&gt; or &lt;code&gt;deleteOne&lt;/code&gt;, pocket-db appends one record to the log and updates the in-memory index pointer. That's it. An update doesn't touch the old record — it writes a replacement which is enough to marks the previous offset as dead. A delete appends a tombstone. This is why &lt;code&gt;deleteOne&lt;/code&gt; reaches 454,000 ops/sec, only marginally behind SQLite in-memory.&lt;/p&gt;

&lt;p&gt;There's a second reason the numbers look good today: &lt;strong&gt;no fsync wait&lt;/strong&gt;. Writes land in the OS page cache and return immediately. This is fast, but it's a trade-off — a hard crash before the OS flushes to disk can lose the last few writes. Pocket-db is perfectly capable to recover from incomplete operations, so consistency is never compromised. For the target use cases (CLI tools, desktop apps, local prototypes), this is an acceptable trade-off. For anything requiring strict durability guarantees, it's something to be aware of.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why reads are slower — and what's being done about it
&lt;/h2&gt;

&lt;p&gt;This is the honest part of the benchmark.&lt;/p&gt;

&lt;p&gt;pocket-db keeps only indexes in memory, not documents. Every &lt;code&gt;findOne&lt;/code&gt; and every cursor step seeks to the document's byte offset in the file and reads from disk. Every result then goes through JSON parsing. This is the fundamental cost of the current design — and it shows clearly in the numbers.&lt;/p&gt;

&lt;p&gt;In-memory stores like lowdb and LokiJS serve reads directly from JavaScript objects: no I/O, no parsing. That's why &lt;code&gt;findById&lt;/code&gt; on a json-file store reaches 12 million ops/sec while pocket-db sits at 142,000.&lt;/p&gt;

&lt;p&gt;Keeping indexes in memory was a necessary first compromise. Without them, reads would require a full file scan for every query — completely unusable in practice. Indexes bring &lt;code&gt;findById&lt;/code&gt; to a competitive level for real-world access patterns where you're not reading the same hot document in a tight loop. But the gap on read-heavy workloads is real, and it needs to be closed.&lt;/p&gt;

&lt;p&gt;The roadmap for the next version is to address this directly. The first candidate is &lt;strong&gt;a document cache&lt;/strong&gt;: keep recently accessed documents in memory and avoid redundant disk reads for hot-document workloads. This alone should close most of the gap for typical usage patterns. The second candidate is a &lt;strong&gt;binary document format&lt;/strong&gt; to replace JSON serialisation — parsing structured binary is significantly faster than &lt;code&gt;JSON.parse&lt;/code&gt; for complex documents. A third option under consideration is &lt;strong&gt;memory-mapped I/O with pagination&lt;/strong&gt;, which would allow the OS to manage the hot-document cache at a lower level.&lt;/p&gt;

&lt;p&gt;The memory footprint is also something to watch. Indexes in memory work well at the scales pocket-db is designed for — thousands to low hundreds of thousands of documents. As collections grow, that in-memory index cost grows with them. Load testing at larger scales is to be considered before locked of the architecture.&lt;/p&gt;


&lt;h2&gt;
  
  
  Reading the numbers in context
&lt;/h2&gt;

&lt;p&gt;The benchmark is deliberately narrow: 1,000 documents, ten operations, one process. That's a fair representation of the workloads pocket-db is built for — a CLI tool tracking state, an Electron app storing user data, a local server caching API responses.&lt;/p&gt;

&lt;p&gt;At that scale, the practical difference between 97 and 870,000 read ops/sec is manageable. A CLI command doesn't loop through &lt;code&gt;findByName&lt;/code&gt; a million times. But it does insert records frequently, update state on every run, and occasionally compact. For that profile, pocket-db's write throughput is genuinely competitive.&lt;/p&gt;

&lt;p&gt;Where the numbers are a real concern is if you're considering pocket-db for a workload that's read-heavy by nature — serving requests in a tight loop, scanning large collections on every query. For those cases today, a pure in-memory store is the honest recommendation.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where things stand
&lt;/h2&gt;

&lt;p&gt;pocket-db is a young project. These benchmark results are encouraging for what it's designed to do: fast, durable writes in a single-file embedded store with a familiar API. The write story is solid. The read story needs work, and the work ahead is well defined.&lt;/p&gt;

&lt;p&gt;The next performance chapter — document cache, binary format, larger-scale index testing — will determine whether pocket-db can compete across the full spectrum of embedded workloads. That's the honest assessment at this first version.&lt;/p&gt;

&lt;p&gt;If you want to run the benchmarks yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/axfab/pocket-db
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if a single-file, zero-dependency document store with a MongoDB-style API sounds like what your next project needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @axfab/pocket-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;&lt;a class="mentioned-user" href="https://dev.to/axfab"&gt;@axfab&lt;/a&gt;/pocket-db is MIT licensed. Contributions, issue reports, and benchmark challenges welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>database</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
