DEV Community

Hasan
Hasan

Posted on

MyNotes: A Lightweight Offline-First Web App with Vanilla JavaScript

1. Building MyNotes

Building a useful web application does not always require a frontend framework, a backend server, or a database.

For this project, I wanted to explore how far I could go using the browser itself as the application platform. The result is MyNotes, a lightweight notes application built with HTML, CSS, and vanilla JavaScript.

MyNotes runs entirely on the client side. Notes are stored locally using localStorage, the application can work offline through a Service Worker, and supported browsers can install it as a Progressive Web App (PWA). It also provides features such as searching, sorting, manual drag-and-drop ordering, light and dark themes, custom note colors, and JSON-based backup and restore.

The project is intentionally simple in its technology stack, but the goal was not to build a simplistic application. Instead, I wanted to see how many features of a practical application could be implemented using standard web technologies and browser APIs without introducing unnecessary complexity.

The result is a cross-platform application that can be opened directly in a browser, installed as a PWA on supported devices, and used without an account or backend infrastructure.

In this article, I will walk through the development of MyNotes, the decisions behind its architecture, how its client-side data storage works, how the PWA functionality is implemented, and some of the challenges involved in building a polished application with vanilla JavaScript.


2. The Idea Behind MyNotes

How much can you actually build with the browser itself?

This question was one of the main motivations behind MyNotes.

When building a modern web application, it is common to start with a frontend framework, a backend API, a database, a package manager, and a build system. These tools are extremely useful for many types of applications, but they can also introduce additional complexity for relatively simple projects.

I wanted to explore a different approach:

What if a useful notes application could be built almost entirely with the capabilities already provided by the browser?

MyNotes was created as an experiment around this idea.

The application does not have a backend server or a remote database. Instead, it uses browser technologies such as localStorage, the File API, the Web Share API, Service Workers, and the Web App Manifest to provide features that would normally require additional infrastructure.

The result is a small, self-contained application that can:

  • Create, edit, and delete notes
  • Search and sort notes
  • Reorder notes using drag and drop
  • Switch between light and dark themes
  • Assign different colors to notes
  • Store data persistently in the browser
  • Back up and restore notes as JSON
  • Work offline after installation
  • Be installed as a Progressive Web App
  • Share backup files through supported mobile browsers

There is also no build pipeline required.

The project can be served as a collection of static files, which makes the architecture relatively easy to understand and the application straightforward to deploy.

2.1 The Goal Was Not to Avoid Modern Tools

This project is not an argument against frameworks, backend services, or databases.

Those technologies solve important problems and are often the right choice for larger applications.

The goal of MyNotes was different: to understand how far the native web platform can go before additional infrastructure becomes necessary.

By keeping the technology stack intentionally small, each part of the application remains visible.

The HTML defines the structure.

The CSS controls the presentation and responsive behavior.

Vanilla JavaScript manages the application state and interactions.

Browser APIs provide persistence, file handling, sharing, and offline capabilities.

A Service Worker handles the offline application shell, while the Web App Manifest makes installation possible.

This makes MyNotes both a practical application and a hands-on exploration of the capabilities of the modern web platform.

The project can be tried directly in the browser through the MyNotes live demo, without creating an account or installing anything.

The complete source code is publicly available on GitHub, including the HTML, CSS, JavaScript, PWA configuration, and supporting project files. You can explore the implementation, review the architecture, or use the project as a reference for building your own offline-first web applications.


3. Why I Built MyNotes

Many notes applications are built around a familiar architecture: a frontend application communicates with a backend, which stores the user's data in a database. This approach is powerful and appropriate for many applications, but it also introduces additional infrastructure, dependencies, and complexity.

I wanted to explore a different approach.

The goal behind MyNotes was to build a practical notes application using the capabilities already available in modern web browsers. Instead of introducing a backend server or a database, the application stores its data locally in the browser. Instead of using a frontend framework, the application is built with HTML, CSS, and vanilla JavaScript.

This was not intended to be a demonstration of how to avoid modern development tools at all costs. The purpose was to understand how far the browser platform itself could be taken before additional layers of technology became necessary.

The result is MyNotes: a lightweight application that can run directly in a browser, work offline, be installed as a Progressive Web App, and provide features such as searching, sorting, drag-and-drop ordering, themes, note colors, and JSON backup and restore.

3.1 Keeping the Architecture Simple

One of the main design decisions was to keep the architecture deliberately small.

There is no:

  • Backend API
  • Database server
  • User account system
  • Frontend framework
  • Package manager
  • Build pipeline

The application consists primarily of an HTML document, a CSS stylesheet, and a JavaScript application layer, supported by a few browser-native technologies such as LocalStorage, Service Workers, and the Web Share API.

This simplicity has an important advantage: most of the application's behavior can be understood by looking directly at the source code.

For a small application such as a personal notes tool, this makes the architecture easier to develop, test, deploy, and maintain.

3.2 A Browser Application, Not a Server Application

Another important decision was to treat the browser as the application's primary runtime environment.

The browser already provides persistent storage, file handling, cryptographic identifier generation, offline capabilities, and integration with operating-system features. MyNotes uses these capabilities instead of recreating them through a custom backend.

This also changes the way data is handled.

The application does not send notes to a remote server. Notes remain in the browser's local storage, while the backup and restore functionality provides a way for users to export their data as JSON and restore it later.

The architecture is therefore intentionally local-first.

3.3 Building Features Without a Framework

Using vanilla JavaScript also made the development process more explicit.

Instead of relying on framework abstractions for application state, rendering, event handling, or component management, MyNotes implements these mechanisms directly.

The application maintains its own state, renders note cards dynamically, handles user interactions through DOM events, and updates LocalStorage whenever persistent data changes.

This approach requires more manual work, but it also provides a clearer view of what is actually happening inside the application.

For me, that was one of the most valuable aspects of the project.

MyNotes was not simply intended to produce a working notes application. It was also an exercise in understanding the browser as an application platform.


4. Application Architecture

MyNotes was designed around a deliberately simple architecture. Instead of introducing a frontend framework, backend API, or database server, the application relies primarily on the browser platform itself.

The application consists of three main layers:

  • HTML — application structure and user interface
  • CSS — visual design, responsive layout, and themes
  • Vanilla JavaScript — application state, data management, and user interactions

Supporting browser technologies provide additional capabilities such as persistent storage, offline operation, file handling, and application installation.

The resulting architecture can be summarized as follows:

                    ┌──────────────────────┐
                    │       MyNotes        │
                    │      Web App         │
                    └──────────┬───────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
              ▼                ▼                ▼
        ┌───────────┐    ┌───────────┐    ┌───────────┐
        │   HTML    │    │    CSS    │    │ JavaScript│
        │ Structure │    │   Design  │    │  Logic    │
        └───────────┘    └───────────┘    └─────┬─────┘
                                                │
                              ┌─────────────────┼─────────────────┐
                              │                 │                 │
                              ▼                 ▼                 ▼
                        ┌────────────┐     ┌────────────┐   ┌────────────┐
                        │localStorage│     │  Service   │   │   Browser  │
                        │   Data     │     │   Worker   │   │    APIs    │
                        └────────────┘     └────────────┘   └────────────┘
Enter fullscreen mode Exit fullscreen mode

4.1 Why Vanilla JavaScript?

One of the primary design decisions was to avoid a frontend framework.

Frameworks such as React, Vue, or Angular can be excellent choices for large and complex applications. However, MyNotes does not require the additional abstraction introduced by a component framework.

The application's core operations are relatively straightforward:

  1. Load notes from local storage.
  2. Maintain the current application state.
  3. Render the notes.
  4. Respond to user interactions.
  5. Update the state.
  6. Persist changes back to local storage.

For this type of application, the browser's native DOM APIs are sufficient.

Using vanilla JavaScript also makes the implementation easier to inspect. There is no build system, transpilation step, dependency management layer, or framework-specific component lifecycle.

The JavaScript code can therefore be executed directly by the browser.

4.2 Application State

The central application state is maintained in JavaScript.

The notes are stored in an array:

let notes = [];
Enter fullscreen mode Exit fullscreen mode

Additional variables track the currently edited note, selected note color, pending confirmation actions, and temporary restore data.

For example:

let currentNoteId = null;
let currentNoteColor = "default";
let pendingAction = null;
let pendingRestoreNotes = null;
Enter fullscreen mode Exit fullscreen mode

This state is intentionally kept simple.

Instead of introducing a dedicated state-management library, MyNotes uses ordinary JavaScript variables and functions to control application behavior.

When the state changes, the interface is rendered again where necessary.

4.3 Rendering Notes

The renderNotes() function is responsible for rebuilding the visible notes area.

The process is conceptually simple:

Application State
       │
       ▼
 Filter Notes
       │
       ▼
 Sort Notes
       │
       ▼
 Generate Card HTML
       │
       ▼
 Insert into DOM
Enter fullscreen mode Exit fullscreen mode

This approach has an important advantage for a small application: the relationship between data and interface remains easy to understand.

A note stored in the application state becomes a note card in the DOM.

The card contains the note title, content, date, color, and other interface elements.

User interaction with the card then leads back to the corresponding note through its unique identifier.

4.4 Separation of Responsibilities

Although MyNotes is intentionally small, the code is organized into logical sections rather than being implemented as one large sequence of event handlers.

For example, app.js separates responsibilities into areas such as:

  • Application initialization
  • Event handling
  • Local storage
  • Searching
  • Sorting
  • Rendering
  • Form validation
  • Note creation
  • Note updating
  • Note deletion
  • Theme management
  • Backup and restore
  • Drag-and-drop
  • Utility functions

This organization makes the code easier to navigate and maintain without requiring a formal architectural framework.

The goal is not to eliminate structure, but to use the minimum structure necessary for the application's requirements.


5. Data Persistence with LocalStorage

One of the most important architectural decisions in MyNotes is the decision to store notes locally.

A traditional notes application might use a backend server and database:

Browser
   │
   ▼
Backend API
   │
   ▼
Database
Enter fullscreen mode Exit fullscreen mode

MyNotes uses a different model:

Browser
   │
   ▼
JavaScript
   │
   ▼
localStorage
Enter fullscreen mode Exit fullscreen mode

This eliminates the need for a backend infrastructure entirely.

5.1 Why LocalStorage?

For the relatively small amount of structured data used by a personal notes application, localStorage provides a practical persistence mechanism.

The application defines a storage key:

const STORAGE_KEY = "notes_app_data";
Enter fullscreen mode Exit fullscreen mode

When the application starts, it reads this value from local storage.

When a note is created, updated, deleted, or reordered, the current notes array is serialized as JSON and written back to storage.

localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
Enter fullscreen mode Exit fullscreen mode

When the application is opened again, the JSON data is parsed and converted back into the notes array.

This provides persistence between browser sessions without requiring a server.

5.2 Advantages of the Client-Side Model

The local-storage approach provides several useful properties.

No account is required.

The application can be opened and used immediately.

No backend infrastructure is required.

There is no server, API, database, authentication system, or hosting backend to maintain.

The application can remain completely client-side.

Notes do not need to be transmitted to a remote server for normal application operation.

The architecture remains inexpensive to host.

Because the application consists of static files, it can be deployed using static hosting services such as GitHub Pages.

However, local storage also introduces an important limitation: data is associated with the browser and device where it was created.

If browser storage is cleared, locally stored notes may be lost.

This is one of the reasons why MyNotes includes a dedicated JSON backup and restore system.

5.3 Local-First Data Storage with localStorage

One of the main design decisions in MyNotes is that note data does not depend on a remote server.

Instead, notes are stored directly in the browser using the Web Storage API and, more specifically, localStorage.

This approach makes the application simple, fast, and independent of network connectivity.

5.4 Why localStorage?

For a small client-side application such as MyNotes, introducing a backend database would add considerable complexity without providing a meaningful benefit for the core use case.

There would be additional components to manage:

  • A backend API
  • A database server
  • Authentication and authorization
  • Network communication
  • Server-side validation
  • Deployment and hosting infrastructure

MyNotes does not need any of these components to perform its primary function.

The application can therefore keep its data entirely on the user's device.

The notes are stored under a dedicated storage key:

const STORAGE_KEY = "notes_app_data";
Enter fullscreen mode Exit fullscreen mode

The selected application theme is stored separately:

const THEME_KEY = "notes_app_theme";
Enter fullscreen mode Exit fullscreen mode

This separation keeps application data and user interface preferences independent.

5.5 Persistent Client-Side State

When MyNotes starts, the application reads the stored notes from localStorage.

The data is parsed from JSON and normalized before being used by the application.

This is important because the application may encounter data created by an earlier version of MyNotes. Rather than assuming that every stored object has exactly the current structure, the application applies default values where necessary.

For example, a note contains fields such as:

{
    id: "...",
    title: "...",
    body: "...",
    color: "default",
    createdAt: "...",
    updatedAt: "..."
}
Enter fullscreen mode Exit fullscreen mode

This gives the application a predictable internal data structure while maintaining compatibility with previously stored notes.

5.6 The Trade-Off of Local Storage

Local storage provides an important benefit: the application works without a backend.

However, it also introduces a fundamental limitation.

The data belongs to the browser storage environment in which it was created. If the user clears the browser's site data, the locally stored notes may be removed.

For that reason, local storage should not be treated as a complete backup strategy.

This is why MyNotes also includes a dedicated JSON backup and restore system.

5.7 JSON Backup and Restore

The backup system provides a simple way to make the user's data portable.

Instead of storing notes only inside the browser, MyNotes can serialize the complete notes array into a JSON file.

The backup preserves the current note data, including the order of notes.

A simplified representation of the generated data looks like this:

[
    {
        "id": "example-id",
        "title": "Example Note",
        "body": "This is an example note.",
        "color": "default",
        "createdAt": "2026-01-01T10:00:00.000Z",
        "updatedAt": "2026-01-01T10:00:00.000Z"
    }
]
Enter fullscreen mode Exit fullscreen mode

The application generates a timestamped filename for the backup, making it easier to identify when a backup was created.

5.8 Browser File APIs

The backup process uses standard browser APIs rather than requiring a server.

The application creates a File object containing the serialized JSON data.

On supported browsers, MyNotes can then use the Web Share API to present the native sharing interface.

This is particularly useful on mobile devices because the backup can be shared using the operating system's normal file-sharing mechanism.

When file sharing is not available, the application falls back to creating a Blob and triggering a standard browser download.

This means the same backup functionality works across different environments without requiring platform-specific code.

5.9 Restore Validation

Restoring a backup is deliberately not implemented as a blind replacement of the current data.

The selected file is first checked to ensure that it is a JSON file.

The contents are then parsed and passed through a validation function.

The application verifies that:

  • The imported data is an array.
  • Each item is an object.
  • Every note has a valid identifier.
  • The title is a string.
  • The body is a string.
  • Missing optional fields can be safely normalized.

Only after the imported data passes validation does MyNotes ask the user for confirmation.

This sequence is important:

Select file
     ↓
Read file
     ↓
Parse JSON
     ↓
Validate data
     ↓
Normalize notes
     ↓
Ask for confirmation
     ↓
Replace current notes
     ↓
Save to localStorage
Enter fullscreen mode Exit fullscreen mode

The confirmation step is especially important because restoring a backup replaces the currently stored notes.

This prevents an accidental file selection from immediately destroying the current local dataset.

5.10 Designing for Offline Use

The local storage architecture also supports the broader offline-first philosophy of MyNotes.

The application does not need to contact a server every time a user creates or edits a note.

Creating a note is a completely local operation:

User action
     ↓
Update application state
     ↓
Save to localStorage
     ↓
Update the interface
Enter fullscreen mode Exit fullscreen mode

There is no network request involved.

As a result, the application can remain useful even when the device has no Internet connection.

This is one of the advantages of building applications directly on top of browser capabilities: functionality that would traditionally require a server can sometimes be implemented entirely on the client.

5.11 Why This Architecture Works for MyNotes

The architecture of MyNotes is intentionally matched to the application's requirements.

The application is:

  • Single-user
  • Client-side
  • Lightweight
  • Data-oriented
  • Suitable for static hosting
  • Designed to work offline
  • Not dependent on centralized synchronization

Under these conditions, a backend database would introduce infrastructure without solving a core requirement.

The result is a much smaller architecture:

                ┌──────────────────────┐
                │      MyNotes UI      │
                │      HTML / CSS      │
                └──────────┬───────────┘
                           │
                           ▼
                ┌──────────────────────┐
                │   Vanilla JavaScript │
                │   Application State  │
                └──────────┬───────────┘
                           │
             ┌─────────────┴─────────────┐
             ▼                           ▼
     ┌────────────────┐         ┌─────────────────┐
     │   localStorage │         │   Browser APIs  │
     │                │         │                 │
     │ Notes          │         │ File API        │
     │ Theme          │         │ Web Share API   │
     └────────────────┘         └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

There is no application server between the user and their notes.

For this particular use case, that simplicity is not a limitation. It is a deliberate architectural choice.


6. Progressive Web App Architecture and the Service Worker

One of the main goals of MyNotes was to make the application feel like a real application rather than simply a web page.

A traditional web application requires the user to open a browser and access the application through a URL. This is perfectly sufficient for many applications, but a notes application has an important additional requirement: it should remain useful even when an internet connection is unavailable.

MyNotes addresses this requirement by using Progressive Web App (PWA) technologies.

A PWA allows a web application to combine the accessibility of the web with several characteristics traditionally associated with native applications. Users can access the application through a browser, install it on supported devices, launch it from the operating system, and continue using it when the network is unavailable.

For MyNotes, this architecture is based primarily on two components:

manifest.json
service-worker.js
Enter fullscreen mode Exit fullscreen mode

6.1 The Web App Manifest

The Web App Manifest provides metadata that describes how the application should behave when it is installed.

In MyNotes, the manifest defines information such as:

  • Application name
  • Short application name
  • Application start URL
  • Application scope
  • Display mode
  • Application language
  • Theme color

A simplified representation looks like this:

{
    "name": "MyNotes",
    "short_name": "MyNotes",
    "start_url": "./index.html",
    "scope": "./",
    "display": "standalone",
    "lang": "en"
}
Enter fullscreen mode Exit fullscreen mode

The display property is particularly important for the application experience.

When standalone mode is supported and the application is installed, MyNotes can be launched without the normal browser interface surrounding the page. From the user's perspective, it behaves much more like a conventional application.

The manifest does not provide offline functionality by itself. Its primary purpose is to describe the application and define how it should be presented when installed.

Offline functionality is provided by the Service Worker.

6.2 The Role of the Service Worker

A Service Worker is a JavaScript file that runs separately from the main browser page and can intercept network requests made by the application.

This makes it possible to control how application resources are retrieved and cached.

The basic architecture can be represented as:

User
  │
  ▼
MyNotes UI
  │
  ▼
Browser
  │
  ├── Online ──────► Network
  │
  └── Offline
          │
          ▼
     Service Worker
          │
          ▼
       Cache
Enter fullscreen mode Exit fullscreen mode

During the initial visit, the Service Worker can cache the resources required by MyNotes.

For example, these resources may include:

index.html
css/style.css
css/bootstrap.min.css
css/bootstrap-icons.css
js/app.js
js/bootstrap.bundle.min.js
manifest.json
favicon.png
Enter fullscreen mode Exit fullscreen mode

Once these resources have been cached, the application can load them from the local cache when the network is unavailable.

This is particularly useful for a notes application because the core data is already stored locally using localStorage.

The application therefore does not need a remote API or database simply to display or modify existing notes.

6.3 Application Data and Application Resources

An important architectural distinction in MyNotes is the separation between application resources and user data.

The Service Worker is responsible for making application resources available offline.

The notes themselves are stored separately in localStorage.

Conceptually:

             MyNotes
                │
        ┌───────┴────────┐
        │                │
        ▼                ▼
 Application          User Data
 Resources            └─ localStorage
        │
        └─ Service Worker
           └─ Cache
Enter fullscreen mode Exit fullscreen mode

This separation keeps the architecture simple.

The Service Worker does not need to manage individual notes. It only needs to ensure that the application itself can be loaded and executed.

The JavaScript application remains responsible for note management, while the browser's storage system remains responsible for persistent local data.

6.4 Why Offline Capability Matters

Offline support is particularly appropriate for MyNotes because note-taking does not inherently require an internet connection.

A user may want to create or read a note:

  • While travelling
  • Without mobile data
  • With an unreliable connection
  • On a device temporarily disconnected from the network

Requiring a backend server for these operations would introduce unnecessary complexity.

Instead, MyNotes follows an offline-first approach: the application is designed around local data and local execution, with network connectivity not being a fundamental requirement for its core functionality.

This is one of the reasons a PWA architecture fits the project particularly well.

6.5 Installing MyNotes as an Application

On supported browsers and devices, the combination of the Web App Manifest and Service Worker allows MyNotes to be installed.

After installation, the application can appear in the device's application launcher and can be opened independently of a normal browser tab.

The same codebase can therefore serve multiple environments:

                  MyNotes
                     │
          ┌──────────┼──────────┐
          │          │          │
          ▼          ▼          ▼
       Desktop     Mobile     Tablet
       Browser      PWA        PWA
Enter fullscreen mode Exit fullscreen mode

No separate Android, iOS, Windows, or macOS application needs to be maintained.

This is one of the major advantages of using web platform capabilities: a single codebase can provide a consistent application across different device categories.

6.6 Keeping the PWA Architecture Simple

MyNotes does not attempt to implement a complicated caching strategy or introduce a framework specifically for PWA management.

The project intentionally keeps the architecture understandable.

The responsibilities remain clearly separated:

index.html
    │
    └── Application structure

style.css
    │
    └── Application presentation

app.js
    │
    └── Application logic and data management

manifest.json
    │
    └── Installation metadata

service-worker.js
    │
    └── Offline resource management
Enter fullscreen mode Exit fullscreen mode

This simplicity is intentional.

The purpose of the project is not to demonstrate the largest possible technology stack, but to show how standard browser capabilities can be combined to create a practical application.

The result is a lightweight application that can be used online, installed as a PWA, and operated offline without requiring a backend infrastructure.


7. Search and Sorting

As the number of notes increases, simply displaying them is no longer enough. Users need a way to quickly locate a specific note and organize their notes according to different criteria.

MyNotes implements both searching and sorting entirely on the client side. No server-side query, database, or external search service is required.

7.1 Searching Notes

The search functionality allows users to search through both the note title and the note content.

Whenever the search field changes, the application calls handleSearch():

function handleSearch() 
{
    const hasSearch = searchInput.value.trim() !== "";
    btnClearSearch.classList.toggle("d-none", !hasSearch);
    renderNotes();
}
Enter fullscreen mode Exit fullscreen mode

The actual filtering is performed by getFilteredNotes():

function getFilteredNotes() 
{
    const search = searchInput.value.trim().toLocaleLowerCase("en-US");

    let result = [...notes];

    if (search) 
    {
        result = result.filter(note => 
        {
            const title = note.title.toLocaleLowerCase("en-US");
            const body = note.body.toLocaleLowerCase("en-US");
            return title.includes(search) || body.includes(search);
        });
    }

    return result;
}
Enter fullscreen mode Exit fullscreen mode

There are several deliberate choices in this implementation.

First, the search is case-insensitive. Both the search query and the note fields are converted to lowercase before comparison.

Second, the search is performed against both the title and body:

title.includes(search) || body.includes(search)
Enter fullscreen mode Exit fullscreen mode

This means that users do not need to remember whether a particular piece of information was stored in the title or in the note content.

Third, the filtering operation works on a copy of the notes array:

let result = [...notes];
Enter fullscreen mode Exit fullscreen mode

This is important because searching should not modify the application's underlying data. The original notes array remains unchanged.

The search is therefore best understood as a view-level operation rather than a data-level operation.

7.2 Search Results and Empty States

The rendering process combines filtering and sorting:

const filteredNotes = sortNotes(getFilteredNotes());
Enter fullscreen mode Exit fullscreen mode

If no notes match the current search query, MyNotes displays a dedicated empty state.

The interface distinguishes between two different situations:

  • There are no notes at all.
  • Notes exist, but none match the search query.

For example, when a search is active, the application displays:

No notes found
No notes match your search.
Enter fullscreen mode Exit fullscreen mode

The application also updates the note counter to reflect the number of visible results:

3 / 12 Notes
Enter fullscreen mode Exit fullscreen mode

This provides immediate feedback about how many notes matched the current query without losing sight of the total number of stored notes.

7.3 Sorting

MyNotes supports several sorting modes:

  • Recently updated
  • Recently created
  • Title A–Z
  • Title Z–A
  • Manual order

The sorting logic is centralized in the sortNotes() function:

function sortNotes(noteList) 
{
    const sortType = sortSelect.value;

    if (sortType === "manual") 
    {
        return noteList;
    }

    return noteList.sort((a, b) => 
    {
        switch (sortType) 
        {
            case "created-desc":
                return compareDates(b.createdAt, a.createdAt);

            case "title-asc":
                return a.title.localeCompare(b.title, "en");

            case "title-desc":
                return b.title.localeCompare(a.title, "en");

            case "updated-desc":
            default:
                return compareDates(b.updatedAt, a.updatedAt);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Date-based sorting uses the timestamps stored with each note:

function compareDates(dateA, dateB) 
{
    return new Date(dateA).getTime() - new Date(dateB).getTime();
}
Enter fullscreen mode Exit fullscreen mode

This allows the application to distinguish between the time a note was originally created and the time it was most recently modified.

For example, the Recently Created option uses createdAt, while Recently Updated uses updatedAt.

Title sorting uses JavaScript's localeCompare():

a.title.localeCompare(b.title, "en")
Enter fullscreen mode Exit fullscreen mode

This provides a more appropriate string comparison than directly comparing strings with relational operators.

7.4 Manual Sorting

Manual sorting is different from the other sorting modes.

Instead of calculating an order from note properties, MyNotes allows the user to determine the order directly using drag and drop.

When manual sorting is selected:

if (sortType === "manual") 
{
    return noteList;
}
Enter fullscreen mode Exit fullscreen mode

The existing order of the notes array is preserved.

This is an important architectural distinction. Manual ordering is not simply another sorting algorithm. It is user-defined persistent ordering.

When the user moves a note, the application determines the resulting order from the DOM and updates the underlying notes array.

updateNotesOrderFromDOM();
saveNotesToStorage();
Enter fullscreen mode Exit fullscreen mode

The new order is therefore persisted in localStorage and remains available after the application is restarted.

7.5 Search and Manual Ordering

Combining search with manual ordering introduces an interesting problem.

Suppose the application contains twelve notes, but a search query displays only four of them. If the user reorders those four notes, the other eight notes should not unexpectedly change position.

MyNotes handles this separately from the normal manual-ordering case.

When no search is active, the entire notes array is rebuilt according to the DOM order.

When a search is active, only the visible notes are reordered:

if (searchInput.value.trim() === "") 
{
    // Rebuild the entire notes array.
    ...
}
Enter fullscreen mode Exit fullscreen mode

Otherwise:

/*
 * When a search is active, reorder only the visible notes.
 */
const visibleSet = new Set(visibleIds);
Enter fullscreen mode Exit fullscreen mode

The application then places the reordered visible notes back into their corresponding positions in the underlying array.

This prevents a filtered view from unintentionally destroying the ordering of notes that are currently hidden.

7.6 Keeping Filtering, Sorting, and Rendering Separate

One of the important design decisions in MyNotes is keeping these responsibilities separate.

The rendering pipeline can be conceptually described as:

notes
  │
  ▼
getFilteredNotes()
  │
  ▼
sortNotes()
  │
  ▼
renderNotes()
  │
  ▼
DOM
Enter fullscreen mode Exit fullscreen mode

The original notes array represents the application's data.

getFilteredNotes() determines which notes should currently be visible.

sortNotes() determines their presentation order.

renderNotes() converts the resulting collection into the user interface.

This separation makes the behavior easier to reason about and reduces the risk of search or sorting operations modifying data unintentionally.

It also makes the interface responsive to changes: whenever the search query or sorting option changes, the application simply renders the appropriate view of the existing data.

7.7 A Fully Client-Side Approach

For a small personal notes application, this architecture is sufficient and has several advantages.

There is no network request when searching.

There is no database query when sorting.

There is no backend synchronization layer.

All operations happen immediately against the notes already stored in the browser.

This is another example of the broader design philosophy behind MyNotes: use the capabilities of the browser platform first, and introduce additional infrastructure only when it is actually necessary.

For the intended scale of the application, client-side filtering and sorting provide a simple, fast, and maintainable solution while keeping the overall architecture lightweight.


8. Drag-and-Drop Note Ordering

One of the more technically interesting features of MyNotes is the ability to manually reorder notes using drag and drop.

At first glance, reordering a collection of cards seems straightforward. In practice, however, a reliable drag-and-drop implementation needs to solve several separate problems:

  • Detecting when the user intends to drag a note
  • Supporting both mouse and touch input
  • Moving the card independently of the document layout
  • Providing a visual indication of where the card will be placed
  • Determining the correct insertion position
  • Updating the application's internal data structure
  • Persisting the new order after the operation is completed

MyNotes implements this functionality without relying on a third-party drag-and-drop library. Instead, it uses the browser's Pointer Events API together with direct DOM manipulation.

8.1 Why Manual Drag-and-Drop?

The application could have used the native HTML5 Drag and Drop API or a dedicated JavaScript library.

However, MyNotes is intentionally built around standard browser APIs. Using Pointer Events provides a unified input model for different pointer devices while keeping the implementation under the application's direct control.

Pointer Events can represent input from:

  • Mouse
  • Touchscreen
  • Pen or stylus

This is particularly useful for MyNotes because the application is designed to work on both desktop computers and mobile devices.

The drag operation is therefore not tied to a specific input device.

8.2 Dragging Starts from a Dedicated Handle

Drag and drop is enabled only when the Manual order sorting option is selected.

In this mode, each note card receives a dedicated drag handle:

const dragHandle = isManualSort
    ? `
        <button
            type="button"
            class="note-card-drag-handle"
            aria-label="Drag to reorder note"
            title="Drag to reorder"
        >
            <i class="bi bi-grip-vertical"></i>
        </button>
    `
    : "";
Enter fullscreen mode Exit fullscreen mode

The handle prevents an important usability problem.

The entire note card remains clickable and can be opened normally, while the handle explicitly indicates the area used for reordering.

A pointer event listener is then attached to the handle:

handle.addEventListener("pointerdown", event => 
{
    if (event.pointerType === "mouse" && event.button !== 0) 
    {
        return;
    }

    if (sortSelect.value !== "manual") 
    {
        return;
    }

    event.preventDefault();
    event.stopPropagation();

    startDrag(event, card, handle, noteId);
});
Enter fullscreen mode Exit fullscreen mode

This also ensures that a right-click or another non-primary mouse action does not accidentally initiate a drag.

8.3 Using a Drag Threshold

A pointer-down event does not immediately mean that the user wants to move the note.

For example, on a touchscreen, a user may touch the handle without intending to perform a reorder operation.

MyNotes therefore introduces a small movement threshold.

The initial pointer position is stored:

startX: event.clientX,
startY: event.clientY,
Enter fullscreen mode Exit fullscreen mode

During subsequent pointer movement, the distance from the starting point is calculated:

const deltaX = event.clientX - dragState.startX;
const deltaY = event.clientY - dragState.startY;
const distance = Math.sqrt( deltaX * deltaX + deltaY * deltaY);
Enter fullscreen mode Exit fullscreen mode

The actual drag does not begin until the pointer has moved at least six pixels:

if (!dragState.dragging && distance < 6) 
{
    return;
}
Enter fullscreen mode Exit fullscreen mode

This small threshold separates a simple pointer interaction from an intentional drag operation.

Once the threshold is exceeded, the application calls:

beginActualDrag();
Enter fullscreen mode Exit fullscreen mode

8.4 Maintaining Drag State

The drag operation is managed through a single state object:

let dragState = null;
Enter fullscreen mode Exit fullscreen mode

When dragging starts, the application stores information such as:

dragState = {
    pointerId: event.pointerId,
    noteId: noteId,
    card: card,
    handle: handle,
    column: column,
    placeholder: null,
    startX: event.clientX,
    startY: event.clientY,
    offsetX: event.clientX - cardRect.left,
    offsetY: event.clientY - cardRect.top,
    width: cardRect.width,
    height: cardRect.height,
    dragging: false
};
Enter fullscreen mode Exit fullscreen mode

This state is important because the card, its Bootstrap grid column, the pointer and the placeholder all have to remain associated throughout the operation.

The pointer identifier is also stored so that unrelated pointer events cannot interfere with the active drag.

8.5 Pointer Capture

The drag handle attempts to capture the pointer:

try 
{
    handle.setPointerCapture(event.pointerId);
} 
catch (error) 
{
    console.warn("Pointer capture could not be started:",error);
}
Enter fullscreen mode Exit fullscreen mode

Pointer capture allows the application to continue receiving pointer events even when the pointer moves outside the original handle.

This is particularly useful when the user drags a note quickly or moves outside the original element.

8.6 The Placeholder

One of the key design decisions in the implementation is the use of a placeholder.

When the actual drag begins, the original Bootstrap grid column is removed from the notes container and replaced by a visually similar placeholder.

The placeholder preserves the space occupied by the dragged note:

const placeholderColumn = document.createElement("div");
placeholderColumn.className = column.className;
placeholderColumn.classList.add("note-card-placeholder-column");
Enter fullscreen mode Exit fullscreen mode

A placeholder element is then placed inside the column:

const placeholder = document.createElement("div");
placeholder.className = "note-card-drag-placeholder";
placeholder.style.width = `${rect.width}px`;
placeholder.style.height = `${rect.height}px`;
Enter fullscreen mode Exit fullscreen mode

The original column is inserted back into the grid only after the drag operation finishes.

This creates an important visual distinction:

The dragged card follows the pointer, while the placeholder represents its future position.

Without a placeholder, the remaining cards would continuously collapse into the space occupied by the dragged card, making the target position much harder to understand.

8.7 Detaching the Card from the Grid

The dragged card is temporarily moved directly into the document body:

document.body.appendChild(card);
Enter fullscreen mode Exit fullscreen mode

This is done intentionally.

The card originally belongs to a Bootstrap grid column. Keeping it inside that layout while dragging could cause its position and dimensions to be influenced by the surrounding grid.

By moving it to the <body> and using fixed positioning, the card becomes independent of the original grid layout.

Its position is then explicitly controlled:

card.style.position = "fixed";
card.style.left = `${rect.left}px`;
card.style.top = `${rect.top}px`;
card.style.width = `${rect.width}px`;
card.style.height = `${rect.height}px`;
card.style.zIndex = "1050";
card.style.pointerEvents = "none";
Enter fullscreen mode Exit fullscreen mode

The application also preserves the pointer's position relative to the card through offsetX and offsetY.

This prevents the card from visually jumping when the drag begins.

8.8 Moving the Card

During the drag, the application listens for pointermove events:

document.addEventListener("pointermove", handleDragPointerMove);
Enter fullscreen mode Exit fullscreen mode

The card position is updated using the current pointer coordinates:

const left = clientX - dragState.offsetX;
const top = clientY - dragState.offsetY;

card.style.left = `${left}px`;
card.style.top = `${top}px`;
Enter fullscreen mode Exit fullscreen mode

The dragged card therefore behaves like a floating representation of the original note.

At the same time, the placeholder position is recalculated.

8.9 Determining the Target Position

The most interesting part of the algorithm is deciding where the placeholder should move.

The application examines the remaining Bootstrap columns:

const columns = Array.from(notesContainer.children).filter(
    column => column !== placeholder
);
Enter fullscreen mode Exit fullscreen mode

For each column, its center point is calculated:

const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
Enter fullscreen mode Exit fullscreen mode

The distance between the pointer and each card is then evaluated.

On desktop layouts, horizontal position receives more importance:

score = horizontalDistance + verticalDistance * 1.5;
Enter fullscreen mode Exit fullscreen mode

On mobile layouts, vertical position becomes more important:

score = verticalDistance + horizontalDistance * 0.25;
Enter fullscreen mode Exit fullscreen mode

This distinction is necessary because the visual arrangement of the cards changes between desktop and mobile layouts.

On desktop, notes are typically displayed in multiple columns. On mobile, they are generally arranged vertically.

The algorithm therefore adapts its interpretation of the pointer position to the current layout.

8.10 Moving the Placeholder

After determining the most appropriate target column, the application decides whether the placeholder should be inserted before or after it.

For desktop layouts, the pointer's horizontal position is used:

insertBefore = clientX < targetRect.left + targetRect.width / 2;
Enter fullscreen mode Exit fullscreen mode

For mobile layouts, the vertical position is used:

insertBefore = clientY < targetRect.top + targetRect.height / 2;
Enter fullscreen mode Exit fullscreen mode

The placeholder is then moved in the DOM:

notesContainer.insertBefore(placeholder, targetColumn);
Enter fullscreen mode Exit fullscreen mode

or:

notesContainer.insertBefore(placeholder, next);
Enter fullscreen mode Exit fullscreen mode

The application also checks whether the placeholder is already in the correct position before modifying the DOM.

This avoids unnecessary DOM operations during pointer movement.

8.11 Finishing the Drag

When the user releases the pointer, the application receives the corresponding pointerup event:

document.addEventListener("pointerup", handleDragPointerUp);
Enter fullscreen mode Exit fullscreen mode

The original Bootstrap column is placed where the placeholder currently resides:

if (placeholder) 
{
    notesContainer.insertBefore(state.column, placeholder);
    placeholder.remove();
}
Enter fullscreen mode Exit fullscreen mode

The card is then moved back into its original column:

state.column.appendChild(card);
Enter fullscreen mode Exit fullscreen mode

Finally, all temporary inline styles are removed:

resetDraggedCardStyles(card);
Enter fullscreen mode Exit fullscreen mode

The card returns to the normal document flow.

8.12 Synchronizing the DOM with Application State

Moving elements in the DOM is not enough.

MyNotes maintains the actual notes in the JavaScript array:

let notes = [];
Enter fullscreen mode Exit fullscreen mode

The DOM represents the current visual state, but the notes array represents the application's persistent state.

Therefore, after the drag operation finishes, the DOM order must be transferred back into the array.

This is handled by:

updateNotesOrderFromDOM();
Enter fullscreen mode Exit fullscreen mode

The function first extracts the note IDs in their current DOM order:

const visibleIds = Array.from(notesContainer.children).map(column =>
    column.querySelector(".note-card")?.dataset.noteId).filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

The application can then use these IDs to reconstruct the correct order of the notes array.

8.13 Handling Search Results

There is an additional complication when the user is currently searching for notes.

Suppose the application contains ten notes, but a search displays only three of them.

If the user reorders those three visible notes, the seven hidden notes must not accidentally disappear or change their relative position.

MyNotes therefore uses two different strategies.

When no search is active, the entire notes array is rebuilt according to the DOM order:

if (searchInput.value.trim() === "") 
{
    const noteMap = new Map(notes.map(note => [note.id, note]));

    const reorderedNotes = visibleIds
        .map(id => noteMap.get(id))
        .filter(Boolean);

    notes = reorderedNotes;
    return;
}
Enter fullscreen mode Exit fullscreen mode

When a search is active, only the visible notes are reordered.

The hidden notes remain in the array:

const visibleSet = new Set(visibleIds);

const reorderedVisibleNotes = visibleIds.map
(
    id => notes.find(note => note.id === id)
);
Enter fullscreen mode Exit fullscreen mode

The application then replaces only the positions occupied by visible notes.

This is an important detail because the filtered DOM is not necessarily a complete representation of the application's data set.

8.14 Persisting the New Order

After the new order has been transferred from the DOM to the notes array, it is written to localStorage:

saveNotesToStorage();
Enter fullscreen mode Exit fullscreen mode

The persistence flow is therefore:

User drags note
       ↓
Pointer Events
       ↓
Dragged card + placeholder
       ↓
New DOM position
       ↓
Read note IDs from DOM
       ↓
Rebuild notes array
       ↓
Save to localStorage
Enter fullscreen mode Exit fullscreen mode

This separation between visual state and application state is an important architectural principle.

The DOM is used to determine what the user did, while the JavaScript data model remains the authoritative source for the application's stored data.

8.15 Preventing Accidental Note Opening

There is one more subtle interaction to handle.

A pointer release after dragging can generate a click event. Since the entire note card is normally clickable, that click could immediately open the note after it has been reordered.

MyNotes prevents this using a temporary flag:

card.dataset.dragged = "true";
Enter fullscreen mode Exit fullscreen mode

The card click handler checks this value:

if (card.dataset.dragged === "true") 
{
    delete card.dataset.dragged;
    return;
}
Enter fullscreen mode Exit fullscreen mode

This allows the same card to remain both draggable and clickable without producing an unwanted modal opening after a drag operation.

8.16 Result

The final implementation combines several browser concepts into a relatively small subsystem:

  • Pointer Events for unified mouse and touch interaction
  • Pointer capture for reliable event tracking
  • A movement threshold to distinguish clicks from drags
  • A placeholder for visual insertion feedback
  • Fixed positioning for the floating card
  • Responsive target-position calculation
  • DOM manipulation for visual ordering
  • Synchronization between DOM order and application state
  • Special handling for filtered search results
  • localStorage persistence
  • Click suppression after a completed drag

The important lesson is that drag and drop is not simply a matter of moving an HTML element.

A robust implementation must keep input events, visual layout, DOM structure, application state, and persistent storage synchronized.

For MyNotes, implementing this functionality with native browser APIs also reinforces one of the project's central design principles: a useful and responsive interaction can often be built directly on top of the web platform without introducing another dependency.


9. Themes and Note Colors

Another important part of MyNotes is its visual customization system.

The application supports both light and dark themes, while individual notes can also have their own colors.

At first glance, changing a theme or assigning a color to a note may seem like a purely visual feature. In practice, however, a theme system affects many parts of the application:

  • Application background
  • Text colors
  • Borders
  • Cards
  • Modals
  • Form controls
  • Buttons
  • Toast notifications
  • Drag-and-drop elements
  • Note-specific colors

MyNotes therefore separates global theme styling from individual note styling.

The global appearance is controlled primarily through CSS variables, while note colors are stored as part of each note's data.

9.1 Why Use CSS Variables?

One of the main requirements of the theme system was to avoid duplicating large amounts of CSS.

Instead of defining completely separate styles for every component in both light and dark modes, MyNotes uses CSS custom properties.

For example:

:root {
    --app-bg: #f5f7fa;
    --surface: #ffffff;
    --surface-secondary: #f8f9fa;
    --border: #dee2e6;
    --text: #212529;
    --text-secondary: #6c757d;
    --card-radius: 12px;
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    --shadow-hover: 0 4px 14px rgba(0, 0, 0, 0.12);
}
Enter fullscreen mode Exit fullscreen mode

Components can then reference these variables instead of hard-coding their colors:

body {
    background-color: var(--app-bg);
    color: var(--text);
}
Enter fullscreen mode Exit fullscreen mode

A note card can use the same system:

.note-card {
    background-color: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
    border-radius: var(--card-radius);
    box-shadow: var(--shadow);
}
Enter fullscreen mode Exit fullscreen mode

This creates a central point of control for the application's visual appearance.

If the theme changes, the components do not need to be individually rewritten.

9.2 Defining the Dark Theme

The dark theme overrides the same CSS variables with different values.

For example:

[data-theme="dark"] {
    --app-bg: #121212;
    --surface: #1e1e1e;
    --surface-secondary: #252525;
    --border: #3a3a3a;
    --text: #f1f1f1;
    --text-secondary: #adb5bd;
}
Enter fullscreen mode Exit fullscreen mode

The important part is that the component styles themselves remain mostly unchanged.

For example, this rule:

.note-card {
    background-color: var(--surface);
    color: var(--text);
}
Enter fullscreen mode Exit fullscreen mode

works in both themes.

In light mode:

--surface → light surface color
--text    → dark text color
Enter fullscreen mode Exit fullscreen mode

In dark mode:

--surface → dark surface color
--text    → light text color
Enter fullscreen mode Exit fullscreen mode

The CSS variables therefore act as an abstraction layer between the component styling and the actual color palette.

9.3 Applying the Theme to the Document

The active theme is represented using a data-theme attribute on the document.

For example:

<html data-theme="dark">
Enter fullscreen mode Exit fullscreen mode

or:

<html data-theme="light">
Enter fullscreen mode Exit fullscreen mode

JavaScript can change the active theme by modifying this attribute:

document.documentElement.dataset.theme = theme;
Enter fullscreen mode Exit fullscreen mode

This approach has an important advantage.

The JavaScript code does not need to know the individual colors used by the interface.

It only needs to know which theme is active.

The actual visual transformation remains the responsibility of CSS.

This keeps the separation between behavior and presentation clear.

9.4 Theme Persistence

Changing the theme would not be very useful if the user's preference disappeared every time the application was closed.

MyNotes therefore stores the selected theme in localStorage.

The application uses a dedicated storage key:

const THEME_KEY = "notes_app_theme";
Enter fullscreen mode Exit fullscreen mode

When the user selects a different theme, the application stores the preference:

localStorage.setItem(THEME_KEY, theme);
Enter fullscreen mode Exit fullscreen mode

When the application starts, it reads the previously selected theme:

const savedTheme = localStorage.getItem(THEME_KEY);
Enter fullscreen mode Exit fullscreen mode

The theme can then be applied:

if (savedTheme) 
{
    document.documentElement.dataset.theme = savedTheme;
}
Enter fullscreen mode Exit fullscreen mode

This allows the application to remember the user's visual preference between sessions.

The selected theme is therefore part of the application's persistent client-side state, even though it does not belong to the notes themselves.

9.5 Separating Theme State from Note State

The application maintains two different kinds of persistent information.

The first is the collection of notes:

let notes = [];
Enter fullscreen mode Exit fullscreen mode

The second is the visual preference:

theme = "light"
Enter fullscreen mode Exit fullscreen mode

These are intentionally stored separately.

The notes are stored using:

const STORAGE_KEY = "notes_app_data";
Enter fullscreen mode Exit fullscreen mode

while the theme is stored using:

const THEME_KEY = "notes_app_theme";
Enter fullscreen mode Exit fullscreen mode

This separation prevents unrelated application state from being mixed together.

For example, changing the theme does not require rewriting the notes collection.

Likewise, restoring a notes backup does not need to modify the user's theme preference.

9.6 Theme Toggle

The user can switch between light and dark mode through the application's theme control.

Conceptually, the operation is simple:

const nextTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.dataset.theme = nextTheme;
localStorage.setItem(THEME_KEY, nextTheme);
Enter fullscreen mode Exit fullscreen mode

The operation therefore consists of three steps:

User changes theme
       ↓
Update data-theme
       ↓
CSS variables change
       ↓
Save preference to localStorage
Enter fullscreen mode Exit fullscreen mode

No individual component needs to be manually recolored by JavaScript.

9.7 Why Theme Colors Belong in CSS

It would be possible to change colors directly from JavaScript:

element.style.backgroundColor = "...";
element.style.color = "...";
Enter fullscreen mode Exit fullscreen mode

However, doing this for every component would quickly become difficult to maintain.

It would also mix presentation logic into application logic.

CSS variables provide a much cleaner model.

JavaScript decides:

Which theme is active?
Enter fullscreen mode Exit fullscreen mode

CSS decides:

What does that theme look like?
Enter fullscreen mode Exit fullscreen mode

This distinction makes the implementation easier to understand and extend.

9.8 Note Color System

Themes control the overall appearance of the application, but MyNotes also allows individual notes to have custom colors.

This is intentionally implemented as a property of the note itself.

Conceptually, a note can contain information such as:

{
    id: "unique-id",
    title: "Project Ideas",
    body: "Ideas for the next version...",
    color: "#fff3cd"
}
Enter fullscreen mode Exit fullscreen mode

The color therefore belongs to the note's data rather than to the global theme.

When a note is rendered, its stored color can be applied to the corresponding card.

For example:

card.style.backgroundColor = note.color;
Enter fullscreen mode Exit fullscreen mode

The exact visual implementation can then be combined with the application's normal card styling.

9.9 Why Store the Note Color?

The note color is not merely a temporary visual effect.

It is part of the user's note customization.

If a user assigns a color to a note and then closes the application, the color should still be present when the note is opened again.

For this reason, the color is stored together with the note in the notes array.

The persistence flow becomes:

User selects note color
       ↓
Update note.color
       ↓
Save notes array
       ↓
localStorage
Enter fullscreen mode Exit fullscreen mode

When the application is rendered again:

localStorage
       ↓
Load notes
       ↓
Read note.color
       ↓
Apply color to card
Enter fullscreen mode Exit fullscreen mode

This makes the color part of the note's persistent state.

9.10 Default Note Colors

Not every note necessarily needs a custom color.

When no custom color has been selected, the application can fall back to the default surface color provided by the current theme.

This is an important distinction.

The global theme determines the default visual environment, while a custom note color overrides that appearance only for the selected note.

Conceptually:

No custom color
       ↓
Use theme surface

Custom color
       ↓
Use note-specific color
Enter fullscreen mode Exit fullscreen mode

This allows the note color system and theme system to coexist without interfering with each other.

9.11 Theme Compatibility

One of the challenges of custom note colors is maintaining readability in both light and dark themes.

A color that looks appropriate in a light interface may have insufficient contrast against dark UI elements.

For this reason, the note card's surrounding UI should remain controlled by the theme variables, while the custom color should primarily affect the note surface.

For example:

.note-card {
    color: var(--text);
    border-color: var(--border);
}
Enter fullscreen mode Exit fullscreen mode

The note-specific background can then be applied independently.

This preserves the global theme for borders, typography and surrounding interface elements.

9.12 Visual State and Persistent State

The theme and note color systems demonstrate the same separation used elsewhere in MyNotes.

There is a difference between what the user currently sees and what the application stores.

For example:

CSS / DOM
    ↓
Current visual appearance
Enter fullscreen mode Exit fullscreen mode

while:

JavaScript state
    ↓
Persistent application data
Enter fullscreen mode Exit fullscreen mode

For a theme:

theme value
    ↓
data-theme attribute
    ↓
CSS variables
    ↓
visual appearance
Enter fullscreen mode Exit fullscreen mode

For a note color:

note.color
    ↓
note rendering
    ↓
card background
Enter fullscreen mode Exit fullscreen mode

In both cases, the visual interface is derived from application state.

9.13 Avoiding Hard-Coded Theme Logic

Another benefit of this architecture is that adding another theme would not require rewriting the application's components.

For example, a future theme could define:

[data-theme="sepia"] {
    --app-bg: ...;
    --surface: ...;
    --surface-secondary: ...;
    --border: ...;
    --text: ...;
    --text-secondary: ...;
}
Enter fullscreen mode Exit fullscreen mode

The existing components could continue using:

background-color: var(--surface);
color: var(--text);
border-color: var(--border);
Enter fullscreen mode Exit fullscreen mode

The application would therefore gain a new visual identity without changing the component implementation.

This is one of the practical advantages of designing the styling system around semantic variables rather than individual color values.

9.14 Result

The final theme and note color system combines several concepts:

  • CSS custom properties for centralized theme values
  • data-theme for selecting the active theme
  • localStorage for theme persistence
  • Separate application state for theme and notes
  • Per-note color properties
  • Persistent note customization
  • Theme-aware default colors
  • Separation between presentation and application logic
  • Semantic styling variables instead of hard-coded component colors

The overall architecture can be summarized as:

                    Application State
                           │
             ┌─────────────┴─────────────┐
             │                           │
          Theme                         Notes
             │                           │
       localStorage                 localStorage
             │                           │
       data-theme                  note.color
             │                           │
             ▼                           ▼
      CSS Variables                Note Rendering
             │                           │
             └─────────────┬─────────────┘
                           ▼
                    Visual Interface
Enter fullscreen mode Exit fullscreen mode

The important lesson is that a theme system does not need to become a large JavaScript subsystem.

By allowing JavaScript to manage state and CSS to manage presentation, MyNotes keeps the implementation relatively small while still supporting persistent themes and customizable note colors.

This approach also demonstrates a broader principle of modern web development: CSS is capable of handling much more application-level visual logic than it initially appears to be. CSS custom properties provide a powerful bridge between application state and presentation without requiring a framework or a large styling architecture.


10. Backup and Restore

One of the most important features of MyNotes is the ability to export notes as a JSON backup file and restore them later. This feature is explaned briefly in Data Persistence with LocalStorage section but here it is explained deeper.

Because MyNotes stores its data entirely in the browser using localStorage, there is no server-side database that can be used to recover notes.

This creates an important requirement:

The user should be able to take their data out of the browser and preserve it independently.

MyNotes solves this problem with a client-side backup and restore system based on standard browser APIs.

The implementation combines several technologies:

  • JSON serialization
  • The File API
  • Client-side validation
  • The Web Share API
  • Browser file downloads
  • localStorage
  • Application-level data normalization

The complete process remains entirely client-side.

No note data is uploaded to a server during backup or restore.

10.1 Why JSON?

JSON was chosen as the backup format because it is lightweight, portable, human-readable, and natively supported by JavaScript.

The application's internal note collection is already represented as a JavaScript array:

let notes = [];
Enter fullscreen mode Exit fullscreen mode

A backup can therefore be created by serializing this array:

const json = JSON.stringify(notes, null, 2);
Enter fullscreen mode Exit fullscreen mode

The second and third arguments are used to format the resulting JSON with indentation.

This makes the backup file easier to inspect manually if necessary.

A simplified backup might look like:

[
    {
        "id": "8b2d...",
        "title": "Shopping List",
        "body": "Milk\nBread\nCoffee",
        "color": "blue",
        "createdAt": 1710000000000,
        "updatedAt": 1710000100000
    },
    {
        "id": "3f91...",
        "title": "Project Ideas",
        "body": "Build a small PWA...",
        "color": "green",
        "createdAt": 1710000200000,
        "updatedAt": 1710000300000
    }
]
Enter fullscreen mode Exit fullscreen mode

The JSON structure preserves the information required to reconstruct the notes.

10.2 Creating the Backup File

After the notes have been serialized, MyNotes creates a Blob containing the JSON data:

const blob = new Blob([json], { type: "application/json" });
Enter fullscreen mode Exit fullscreen mode

A Blob represents immutable raw data that can be handled by browser APIs.

The application can then create a temporary object URL:

const url = URL.createObjectURL(blob);
Enter fullscreen mode Exit fullscreen mode

This URL provides a browser-accessible representation of the generated backup file.

A temporary download element can then be created:

const link = document.createElement("a");

link.href = url;
link.download = "mynotes-backup.json";
Enter fullscreen mode Exit fullscreen mode

The application programmatically triggers the download:

link.click();
Enter fullscreen mode Exit fullscreen mode

After the operation is complete, the temporary object URL can be released:

URL.revokeObjectURL(url);
Enter fullscreen mode Exit fullscreen mode

The entire file-generation process therefore happens locally in the browser.

No server-side file-generation process is required.

10.3 Generating a Meaningful File Name

A backup file should be easy for the user to identify later.

Instead of using a generic filename, the application can include the current date and time:

const now = new Date();
const date = now.toISOString().slice(0, 10);
const time = now.toTimeString().slice(0, 8).replaceAll(":", "-");
const fileName = `my-notes-backup-${date}-${time}.json`;
Enter fullscreen mode Exit fullscreen mode

For example:

my-notes-backup-2026-08-31-13-24-20.json
Enter fullscreen mode Exit fullscreen mode

This makes it easier to keep multiple backups and determine when each backup was created.

10.4 Reading a Backup File

Restoring a backup starts with a standard file input:

<input
    type="file"
    id="restoreFileInput"
    accept=".json,application/json"
>
Enter fullscreen mode Exit fullscreen mode

The accept attribute helps guide the user toward selecting a JSON backup file.

When the user selects a file, the application receives a File object.

The browser's File API can then be used to read its contents.

One straightforward approach is:

const text = await file.text();
Enter fullscreen mode Exit fullscreen mode

This returns the complete contents of the selected file as a string.

The application can then attempt to parse the JSON:

let importedNotes;

try 
{
    importedNotes = JSON.parse(text);
} 
catch (error) 
{
    // Invalid JSON
}
Enter fullscreen mode Exit fullscreen mode

At this stage, however, successfully parsing JSON does not mean that the file is a valid MyNotes backup.

This distinction is important.

10.5 JSON Parsing Is Not Validation

A JSON file can be syntactically valid while containing completely unrelated data.

For example:

{
    "hello": "world"
}
Enter fullscreen mode Exit fullscreen mode

This is valid JSON, but it is not a valid MyNotes backup.

Likewise, an array could contain objects with missing or invalid properties.

Therefore, the restore process needs a separate validation stage.

The application first verifies that the parsed value has the expected top-level structure.

For example:

if (!Array.isArray(importedNotes)) 
{
    throw new Error("Invalid backup format.");
}
Enter fullscreen mode Exit fullscreen mode

The application can then validate individual notes.

A note should contain the expected properties and compatible data types.

Conceptually, the validation process checks properties such as:

id
title
body
color
createdAt
updatedAt
Enter fullscreen mode Exit fullscreen mode

The exact validation rules should match the application's internal note model.

10.6 Validating Imported Notes

Validation is particularly important because imported data should never be trusted simply because it came from a file generated by the application.

A user could accidentally select another JSON file.

The file could also have been manually modified or become corrupted.

A validation function can therefore reject invalid note objects:

function isValidNote(note) 
{
    return (
        note &&
        typeof note === "object" &&
        typeof note.id === "string" &&
        typeof note.title === "string" &&
        typeof note.body === "string"
    );
}
Enter fullscreen mode Exit fullscreen mode

The application can then validate the complete collection:

if (!importedNotes.every(isValidNote)) 
{
    throw new Error("Invalid note data.");
}
Enter fullscreen mode Exit fullscreen mode

This provides a boundary between external file data and the application's internal state.

The important principle is:

Never directly replace application state with unvalidated imported data.

10.7 Normalizing Imported Data

Validation answers the question:

"Is this data acceptable?"

Normalization answers a slightly different question:

"Can this data be converted into the exact structure expected by the application?"

For example, an imported note may contain optional properties, older fields, or values that need to be converted into the application's current representation.

A normalization step can create a clean internal object:

const normalizedNote = {
    id: note.id,
    title: note.title,
    body: note.body,
    color: note.color,
    createdAt: note.createdAt,
    updatedAt: note.updatedAt
};
Enter fullscreen mode Exit fullscreen mode

The application can also apply default values where appropriate.

This creates a controlled transition:

File
  ↓
Text
  ↓
JSON.parse()
  ↓
Validation
  ↓
Normalization
  ↓
Application state
Enter fullscreen mode Exit fullscreen mode

Keeping these stages separate makes the restore process easier to reason about and maintain.

10.8 Confirming the Restore Operation

Restoring a backup replaces the current note collection.

Therefore, it is a potentially destructive operation.

If the user currently has notes in the application and restores an older backup, the current collection may be replaced by the contents of that backup.

MyNotes therefore requires user confirmation before completing the operation.

The general flow is:

User selects backup
       ↓
Read file
       ↓
Parse JSON
       ↓
Validate data
       ↓
Normalize data
       ↓
Show confirmation
       ↓
Replace current notes
       ↓
Save to localStorage
       ↓
Refresh interface
Enter fullscreen mode Exit fullscreen mode

This confirmation step prevents an accidental file selection from immediately overwriting the current data.

10.9 Replacing Application State

Once the imported data has passed validation and the user has confirmed the operation, the application can replace the current notes:

notes = normalizedNotes;
Enter fullscreen mode Exit fullscreen mode

The new collection is then persisted:

saveNotesToStorage();
Enter fullscreen mode Exit fullscreen mode

Finally, the interface is rendered again:

renderNotes();
Enter fullscreen mode Exit fullscreen mode

The important point is that the application does not attempt to manipulate individual note cards during restore.

Instead, it updates the application's data model and lets the normal rendering process rebuild the interface.

This keeps the data flow consistent with the rest of the application.

10.10 Using the Web Share API

On supported mobile browsers, MyNotes can provide a more natural way to handle backup files through the Web Share API.

The API allows web applications to invoke the device's native sharing interface.

Before using it, the application checks whether file sharing is supported:

if 
(
    navigator.share &&
    navigator.canShare &&
    navigator.canShare({ files: [file] })
) 
{
    // Share the file
}
Enter fullscreen mode Exit fullscreen mode

The backup file can then be passed to the native sharing mechanism:

await navigator.share({
    title: "MyNotes Backup",
    text: "MyNotes backup file",
    files: [file]
});
Enter fullscreen mode Exit fullscreen mode

On a supported mobile device, this can open the operating system's normal share interface.

The user can then choose an available destination such as a messaging application, cloud storage provider, email application, or another compatible target.

This provides a much more natural mobile experience than forcing the user to locate a downloaded file manually.

10.11 Why Web Share API Support Is Optional

The Web Share API is not universally available across all browsers and platforms.

Even when navigator.share exists, file sharing may not be supported.

This is why the application does not depend on the API being available.

Instead, it treats Web Share as an enhancement.

The logic is conceptually:

Can share files?
      │
      ├── Yes → Use native sharing
      │
      └── No  → Use standard file download
Enter fullscreen mode Exit fullscreen mode

This follows an important web development principle:

Use progressive enhancement rather than making optional browser capabilities mandatory.

10.12 Desktop Fallback

Desktop browsers may not provide file sharing through the Web Share API.

In that situation, MyNotes falls back to a conventional browser download.

The user still receives exactly the same JSON backup file.

The difference is only in how the file is delivered.

On a supported mobile browser:

Create backup
      ↓
Web Share API
      ↓
Native sharing interface
Enter fullscreen mode Exit fullscreen mode

On a desktop browser without file sharing:

Create backup
      ↓
Create Blob
      ↓
Create object URL
      ↓
Trigger download
Enter fullscreen mode Exit fullscreen mode

This allows the same backup functionality to work across different environments without requiring platform-specific code.

10.13 Handling Restore Errors

File operations can fail for several reasons.

For example:

  • The selected file may not contain valid JSON.
  • The JSON structure may not match the expected format.
  • A note may contain invalid properties.
  • The file may have been manually modified.
  • The user may cancel the file-selection operation.

The restore process therefore needs controlled error handling.

A simplified structure is:

try 
{
    const text = await file.text();
    const importedNotes = JSON.parse(text);

    // Validate
    // Normalize
    // Confirm
    // Restore
} 
catch (error) 
{
    showToast("The selected backup file is invalid.", "error");
}
Enter fullscreen mode Exit fullscreen mode

The application should report the problem to the user without modifying the existing notes.

This is particularly important.

A failed restore operation must not corrupt or partially replace the current application state.

10.14 Keeping the Current Data Safe

The restore process follows a transactional mindset.

The existing notes should remain untouched until all of the following conditions have been satisfied:

  1. The file was successfully read.
  2. The JSON was successfully parsed.
  3. The data structure was validated.
  4. The imported notes were normalized.
  5. The user explicitly confirmed the operation.

Only then should the application execute:

notes = normalizedNotes;
Enter fullscreen mode Exit fullscreen mode

This ordering significantly reduces the risk of accidental data loss.

The conceptual model is:

Current State
     │
     │
     ├── Read external file
     │
     ├── Parse
     │
     ├── Validate
     │
     ├── Normalize
     │
     ├── Confirm
     │
     ▼
New State
Enter fullscreen mode Exit fullscreen mode

The current state remains authoritative until the new state is known to be valid.

10.15 Backup and Restore as a Complete Data Pipeline

The backup and restore system can be viewed as two complementary pipelines.

The backup pipeline is:

notes array
    ↓
JSON.stringify()
    ↓
JSON text
    ↓
Blob
    ↓
File
    ↓
Web Share API / Download
Enter fullscreen mode Exit fullscreen mode

The restore pipeline is the reverse:

File
    ↓
File API
    ↓
Text
    ↓
JSON.parse()
    ↓
Validation
    ↓
Normalization
    ↓
User confirmation
    ↓
notes array
    ↓
localStorage
    ↓
UI rendering
Enter fullscreen mode Exit fullscreen mode

This makes the architecture relatively easy to understand.

The backup system converts internal application state into a portable external representation.

The restore system converts that external representation back into validated application state.

10.16 Browser Storage and Portable Data

There is an important architectural distinction between localStorage and JSON backups.

localStorage provides persistent local application state.

The JSON file provides portable application data.

They solve different problems.

localStorage
    ↓
Persistent on this browser/device

JSON backup
    ↓
Portable between browsers/devices
Enter fullscreen mode Exit fullscreen mode

For example, clearing browser data can remove the application's locally stored notes.

A previously created JSON backup, however, remains an independent file.

This is why backup functionality is particularly valuable for an application that intentionally avoids a remote database.

10.17 Result

The final backup and restore implementation combines several browser technologies into a complete client-side data management system:

  • JSON for a portable data format
  • JSON.stringify() for serialization
  • JSON.parse() for deserialization
  • Blob for generating backup files
  • The File API for reading selected files
  • Validation for protecting application state
  • Normalization for producing a consistent internal structure
  • Confirmation dialogs for destructive restore operations
  • The Web Share API for native mobile file sharing
  • Standard browser downloads as a desktop fallback
  • localStorage for persistent local storage

The important lesson is that client-side storage does not have to mean that data is trapped inside the browser.

By combining browser storage with a well-defined external file format, an application can remain completely backend-free while still providing a practical mechanism for data portability, preservation, and recovery.

For MyNotes, this is an important part of the overall architecture: the application keeps its data local by default, but gives the user control over when and how that data leaves the browser.


11. Security Considerations

Although MyNotes is a relatively small client-side application, security still needs to be considered carefully.

The application does not have a backend, authentication system, or remote database. Notes remain inside the browser and are not intentionally transmitted to a server.

However, client-side applications are not automatically secure simply because they do not have a backend.

MyNotes therefore applies several security-related principles around:

  • User-generated content
  • HTML injection
  • Local data storage
  • Imported backup files
  • Browser storage limitations
  • Client-side validation
  • Data privacy

The goal is not to provide enterprise-level security, but to avoid introducing unnecessary security risks into a local-first application.

11.1 Local-Only Data

One of the most important security characteristics of MyNotes is that note data remains local to the browser.

There is no backend API such as:

Browser
   ↓
HTTP Request
   ↓
Backend API
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Instead, the architecture is essentially:

Browser
   ↓
JavaScript Application
   ↓
localStorage
Enter fullscreen mode Exit fullscreen mode

This means the application does not need to transmit note content to a remote server.

For example, creating a note does not result in a request such as:

fetch("/api/notes", {
    method: "POST",
    body: JSON.stringify(note)
});
Enter fullscreen mode Exit fullscreen mode

The note is stored locally:

localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
Enter fullscreen mode Exit fullscreen mode

This significantly reduces the application's network attack surface.

There are no server-side endpoints for attackers to target, and there is no remote database containing the user's notes.

However, local-only storage should not be interpreted as absolute protection.

The data is still accessible to JavaScript running within the application's origin, and anyone with access to the user's browser profile or device may potentially access browser-stored data.

11.2 User-Generated Content and HTML Injection

Notes contain user-controlled data.

A note can contain arbitrary text entered by the user, including characters that have special meaning in HTML.

For example:

<script>alert("Hello")</script>
Enter fullscreen mode Exit fullscreen mode

If this string were inserted directly into an HTML template, the browser could interpret it as markup rather than ordinary text.

This could create an HTML injection or cross-site scripting (XSS) vulnerability.

For this reason, MyNotes does not blindly insert note content into generated HTML.

Instead, user-controlled values are passed through an escapeHtml() helper before being inserted into the interface.

A simplified example is:

function escapeHtml(value) 
{
    return String(value)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}
Enter fullscreen mode Exit fullscreen mode

The important characters are converted into their HTML entity equivalents.

For example:

<script>alert("Hello")</script>
Enter fullscreen mode Exit fullscreen mode

becomes conceptually:

&lt;script&gt;alert(&quot;Hello&quot;)&lt;/script&gt;
Enter fullscreen mode Exit fullscreen mode

The browser therefore renders the content as text instead of interpreting it as executable HTML.

11.3 Escaping Before Rendering

The security boundary is particularly important because MyNotes dynamically generates note cards.

A note card may contain values such as:

note.title
note.body
note.color
note.id
Enter fullscreen mode Exit fullscreen mode

These values originate from application data and, in some cases, directly from user input.

When constructing the card HTML, user-controlled textual values are escaped:

const title = escapeHtml(note.title);
const body = escapeHtml(note.body);
Enter fullscreen mode Exit fullscreen mode

The escaped values can then be included in the generated markup.

This creates a simple data flow:

User Input
    ↓
Note Object
    ↓
escapeHtml()
    ↓
Generated HTML
    ↓
DOM
Enter fullscreen mode Exit fullscreen mode

The important principle is that data should not automatically be treated as markup.

11.4 Why Client-Side Validation Is Not a Security Boundary

MyNotes also performs client-side validation.

For example, title and note content have maximum lengths:

const MAX_TITLE_LENGTH = 200;
const MAX_BODY_LENGTH = 5000;
Enter fullscreen mode Exit fullscreen mode

Validation can prevent invalid or unnecessarily large input from entering the application.

However, client-side validation should not be confused with a complete security mechanism.

Because MyNotes runs entirely in the browser, a technically capable user can modify the JavaScript, manipulate the DOM, or directly modify localStorage.

For example, a user could manually execute:

localStorage.setItem("notes_app_data", "modified data");
Enter fullscreen mode Exit fullscreen mode

The application therefore treats validation primarily as a way to maintain data integrity and provide a good user experience.

There is no server-side trust boundary that needs to be protected because the application does not have a backend.

11.5 localStorage Is Not a Secure Storage Mechanism

localStorage is convenient, simple, and well suited to a lightweight notes application.

However, it should not be considered a secure or encrypted database.

Data stored using:

localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
Enter fullscreen mode Exit fullscreen mode

is generally accessible to JavaScript running in the same origin.

The stored data is not automatically encrypted.

This means MyNotes should not be presented as a secure vault for highly sensitive information such as:

  • Passwords
  • Authentication tokens
  • Credit card numbers
  • Private encryption keys
  • Highly confidential credentials

The purpose of localStorage in MyNotes is persistent application data, not cryptographic protection.

11.6 Browser Storage Is Origin-Based

Another important characteristic of localStorage is that storage is associated with the application's origin.

For example, the deployed MyNotes application runs under its own web origin.

Notes stored by that application are therefore associated with that origin and browser profile.

This has two important consequences.

First, another unrelated website cannot normally access the same localStorage data because of the browser's same-origin policy.

Second, moving to another browser or device does not automatically move the notes.

For example:

Desktop Chrome
    ↓
MyNotes localStorage
Enter fullscreen mode Exit fullscreen mode

and:

Mobile Chrome
    ↓
MyNotes localStorage
Enter fullscreen mode Exit fullscreen mode

represent separate local storage environments.

This is one of the reasons the backup and restore functionality is important.

11.7 Clearing Browser Data

Because MyNotes stores its notes locally, clearing browser storage can remove application data.

For example, if a user clears site data or browser storage for the application's origin, the notes stored in localStorage may be deleted.

This is fundamentally different from a cloud-based notes application.

A cloud application may store the data remotely and synchronize it after the user signs in.

MyNotes does not have such a recovery mechanism.

The application therefore explicitly provides JSON backup and restore functionality so users can create an independent copy of their data.

The recommended data flow is:

MyNotes
   ↓
Export JSON Backup
   ↓
Store Backup Safely
Enter fullscreen mode Exit fullscreen mode

If necessary:

JSON Backup
   ↓
Import
   ↓
Validation
   ↓
MyNotes
Enter fullscreen mode Exit fullscreen mode

This gives the user control over their own data without requiring a server.

11.8 Backup Files Are User-Controlled Data

The JSON restore mechanism introduces another security consideration.

A backup file is external data.

It should therefore not be assumed to be trustworthy simply because it uses the .json extension.

The restore process first reads the selected file:

const text = await file.text();
Enter fullscreen mode Exit fullscreen mode

The contents are then parsed:

const data = JSON.parse(text);
Enter fullscreen mode Exit fullscreen mode

Parsing the JSON is only the first step.

The imported data must also be validated before it replaces the existing notes.

Conceptually, the process is:

Selected File
      ↓
Read File
      ↓
Parse JSON
      ↓
Validate Structure
      ↓
Normalize Data
      ↓
Ask for Confirmation
      ↓
Replace Current Data
Enter fullscreen mode Exit fullscreen mode

This prevents arbitrary JSON structures from being treated as valid application state.

11.9 Imported Data Must Be Treated as Untrusted

Even though the backup file is generated by MyNotes, users can edit it manually or receive a file from another source.

For example, a malicious or malformed file could contain unexpected values:

{
    "id": null,
    "title": 12345,
    "body": {},
    "color": "<script>"
}
Enter fullscreen mode Exit fullscreen mode

The application should therefore verify the expected structure before accepting imported data.

Validation can check properties such as:

  • The imported value is an array.
  • Each note is an object.
  • Required fields exist.
  • IDs have an acceptable format.
  • Titles and bodies have valid types.
  • String lengths remain within application limits.
  • Optional properties are normalized to safe defaults.

This is particularly important because imported data eventually becomes part of the application's internal state.

11.10 Normalization of Imported Data

Validation and normalization are closely related but serve different purposes.

Validation determines whether data is acceptable.

Normalization converts acceptable data into the format expected by the application.

For example:

Imported Data
     ↓
Validation
     ↓
Normalization
     ↓
Internal Note Model
Enter fullscreen mode Exit fullscreen mode

A normalized note should have predictable properties.

This reduces the number of unexpected states that the rest of the application needs to handle.

It also means rendering code can operate on a consistent data model instead of repeatedly checking whether every property exists.

11.11 Confirmation Before Replacing Data

Restoring a backup can replace the current notes.

This is not necessarily a security vulnerability, but it is an important data-integrity consideration.

MyNotes therefore requests confirmation before replacing the current dataset.

The operation follows this pattern:

Select Backup
      ↓
Read File
      ↓
Parse
      ↓
Validate
      ↓
Normalize
      ↓
User Confirmation
      ↓
Replace Notes
Enter fullscreen mode Exit fullscreen mode

The confirmation step gives the user an opportunity to cancel before existing data is overwritten.

This is particularly important because the application has no remote database from which the previous state can automatically be recovered.

11.12 Security and the Service Worker

The Service Worker introduces another important consideration.

A Service Worker has significant control over network requests and cached application resources within its scope.

MyNotes uses the Service Worker primarily to cache the application's static resources so that the application can continue working offline.

Conceptually:

Browser
   ↓
Service Worker
   ↓
Cached Application Resources
   ↓
MyNotes
Enter fullscreen mode Exit fullscreen mode

The Service Worker should therefore only cache resources that are intentionally part of the application.

It should not be used as a mechanism for storing sensitive user data.

The separation is intentional:

Application Files
        ↓
Service Worker Cache

User Notes
        ↓
localStorage
Enter fullscreen mode Exit fullscreen mode

This keeps application resources and user data logically separate.

11.13 No Authentication Means No Account Security

MyNotes does not provide user accounts.

There is therefore no:

  • Login system
  • Password storage
  • Session management
  • Authentication token
  • Password reset mechanism
  • Server-side authorization layer

This removes an entire class of security problems that would normally exist in a server-based application.

At the same time, it means MyNotes cannot provide account-based access control.

Anyone who has access to the browser profile where the notes are stored may potentially access those notes.

This is an inherent trade-off of the local-first architecture.

11.14 The Same-Origin Policy Still Matters

Although MyNotes does not communicate with a backend, it still operates within the browser's security model.

The browser's same-origin policy restricts how scripts from different origins interact with one another.

For MyNotes, this provides an important isolation boundary around:

  • localStorage
  • DOM content
  • Application resources
  • Service Worker scope

The application therefore benefits from browser security mechanisms without having to implement an equivalent isolation system itself.

11.15 Security Through Simplicity

One of the useful characteristics of MyNotes is that its architecture reduces the number of security-sensitive components.

A traditional full-stack notes application might contain:

Frontend
   ↓
Authentication
   ↓
API
   ↓
Authorization
   ↓
Backend
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

Each layer introduces additional security requirements.

MyNotes intentionally removes most of these layers:

Browser
   ↓
MyNotes
   ↓
localStorage
Enter fullscreen mode Exit fullscreen mode

This does not make the application automatically secure.

Instead, it reduces the number of places where security failures can occur.

The remaining responsibilities become much more focused:

  • Treat user input as untrusted.
  • Escape dynamic HTML content.
  • Validate imported data.
  • Avoid storing secrets in localStorage.
  • Clearly communicate the limitations of local storage.
  • Provide backups for data recovery.
  • Keep Service Worker behavior limited to its intended purpose.

11.16 Practical Security Model

The resulting security model can be summarized as follows:

                 MyNotes
                    │
          ┌─────────┴─────────┐
          │                   │
      User Input         Imported Data
          │                   │
          ↓                   ↓
    escapeHtml()         Validation
          │                   │
          └─────────┬─────────┘
                    ↓
               Application
                    │
                    ↓
              localStorage
Enter fullscreen mode Exit fullscreen mode

The application does not attempt to solve problems that belong to a server architecture.

Instead, it focuses on the risks that actually exist in a client-side application.

11.17 Security Trade-Offs of a Local-First Application

The local-first architecture provides several advantages:

  • No remote storage of notes
  • No backend attack surface
  • No account credentials
  • No authentication database
  • No server-side API
  • No network dependency for normal operation

But it also has limitations:

  • localStorage is not encrypted.
  • Browser data can be deleted.
  • Notes are not automatically synchronized between devices.
  • Anyone with access to the browser profile may potentially access the data.
  • There is no server-side authorization mechanism.
  • Backup files must be protected by the user.

These trade-offs are not accidental.

They are direct consequences of the application's architectural goal: keep the application simple, local, and independent of a backend.

11.18 Result

The security implementation in MyNotes is intentionally lightweight, but it is not ignored.

The application combines several techniques:

  • HTML escaping with escapeHtml()
  • Client-side input validation
  • Validation and normalization of imported JSON data
  • Confirmation before replacing stored data
  • Local-only note storage
  • Awareness of localStorage limitations
  • Browser same-origin protections
  • Limited Service Worker responsibilities
  • No unnecessary authentication or server-side components

The most important principle is that client-side simplicity does not eliminate the need for security considerations.

Even a small application must distinguish between trusted application logic and untrusted user data.

For MyNotes, this means treating note content and imported files as data rather than executable code, while also being transparent about the limitations of browser-local storage.

The result is a security model that matches the application's architecture: simple, local, dependency-light, and focused on protecting the boundaries that actually exist.


12. What I Learned

Building MyNotes was a relatively small project in terms of code size, but it turned out to be a useful exercise in making architectural decisions without relying on a backend, framework, database, or build system.

The project reinforced several lessons that are applicable well beyond a simple notes application.

12.1 Simplicity Is an Architectural Decision

One of the most important lessons was that avoiding unnecessary complexity can be a deliberate architectural choice.

MyNotes does not use:

  • A backend server
  • A REST API
  • A database server
  • A frontend framework
  • A package manager
  • A bundler
  • A build pipeline

Instead, the application is built around browser-native technologies and APIs.

HTML
  +
CSS
  +
Vanilla JavaScript
  +
Browser APIs
  +
Local Storage
  +
Service Worker
  +
Web App Manifest
Enter fullscreen mode Exit fullscreen mode

This approach significantly reduces the number of moving parts.

There are fewer dependencies to manage, fewer deployment requirements, and fewer external services that can fail.

However, simplicity does not mean that architecture becomes unimportant.

In fact, without a framework or backend handling parts of the application automatically, more responsibility moves into the application code itself.

For example, the application has to explicitly manage:

  • Application state
  • Persistence
  • Validation
  • DOM updates
  • Drag-and-drop state
  • Theme state
  • Backup and restore
  • Offline caching
  • Data synchronization between the UI and localStorage

The lesson is that removing infrastructure does not remove complexity. It changes where the complexity lives.

12.2 Browser APIs Are More Capable Than They May First Appear

Another important lesson was how much functionality is already available directly in modern browsers.

MyNotes uses several browser capabilities without requiring third-party services.

For example:

Requirement Browser capability
Persistent note data localStorage
Offline support Service Worker
Installable application Web App Manifest
File export Blob + File APIs
File import File API
Mobile sharing Web Share API
Drag interactions Pointer Events
Unique identifiers crypto.randomUUID()
Responsive UI CSS + Bootstrap

This changed the way I approached the problem.

Instead of asking:

Which library should I install?

I could first ask:

Does the browser already provide this capability?

That distinction is important.

Third-party libraries are useful when they solve a genuinely difficult problem or significantly improve productivity. But adding a dependency for functionality that the platform already provides can increase the project's maintenance burden without providing much value.

For a small application, using native browser capabilities can therefore be both a technical and architectural advantage.

12.3 Local-First Changes the Data Model

Using localStorage initially looks extremely simple:

localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
Enter fullscreen mode Exit fullscreen mode

But once the application grows beyond basic CRUD operations, persistence becomes an architectural concern.

The application has to decide:

  • What data should be persisted?
  • Which fields are required?
  • How are IDs generated?
  • What happens when stored data is malformed?
  • What happens when an older version of the application reads newer data?
  • How should imported data be validated?
  • How should the application behave when storage is unavailable?
  • How should the user recover their data?

This led to an important realization:

Persistence is not simply about saving data. It is about defining the lifecycle of that data.

The same principle applies to backup and restore.

Once data exists only in the browser, exporting that data becomes an important part of the application's reliability model.

12.4 UI State and Application State Must Stay Synchronized

The drag-and-drop implementation reinforced another important lesson: the DOM is not necessarily the application's source of truth.

For example, after a user manually reorders notes, changing the visual order of cards is not enough.

The application state must also be updated.

Conceptually:

User interaction
       ↓
DOM changes
       ↓
Application state changes
       ↓
Persistent storage changes
Enter fullscreen mode Exit fullscreen mode

If only the DOM changes, the new order can disappear as soon as the application re-renders.

This becomes particularly important when features such as search and sorting are introduced.

The visible DOM may represent only a subset of the complete notes array.

Therefore, operations such as drag-and-drop cannot blindly assume:

DOM order === complete application state
Enter fullscreen mode Exit fullscreen mode

The application needs an explicit strategy for translating UI interactions back into application data.

This is a general lesson that applies to many interactive applications, not just note-taking applications.

12.5 Mobile Interaction Requires More Than Responsive CSS

Making an application responsive is not limited to changing widths and stacking elements.

The interaction model itself may need to change.

The drag-and-drop functionality was a good example.

A mouse-oriented implementation could rely on mouse events, but a mobile application needs to consider touch input as well.

Using Pointer Events provides a unified model:

Mouse
  \
Touch ----> Pointer Events
  /
Pen
Enter fullscreen mode Exit fullscreen mode

This makes the interaction layer more consistent across devices.

However, supporting mobile interaction also required thinking about:

  • Drag thresholds
  • Accidental clicks
  • Pointer capture
  • Touch-friendly drag handles
  • Visual feedback
  • Placeholder positioning
  • Different card dimensions
  • Different grid layouts

The lesson was that responsive design and responsive interaction are two different problems.

An interface can look correct on a phone while still feeling wrong when interacted with on a touch screen.

12.6 Offline Support Is More Than Adding a Service Worker

Adding a Service Worker is often presented as the main step toward offline functionality.

In practice, offline-first behavior is a broader architectural decision.

The Service Worker handles resource availability:

Browser
   │
   ├── HTML
   ├── CSS
   ├── JavaScript
   └── Bootstrap resources
          │
          ▼
     Service Worker
          │
          ▼
        Cache
Enter fullscreen mode Exit fullscreen mode

But cached application resources alone do not make an application fully useful offline.

The application data also needs to remain available.

For MyNotes, this means that two separate concerns work together:

Application resources
        ↓
   Service Worker
        ↓
      Cache

Application data
        ↓
    localStorage
Enter fullscreen mode Exit fullscreen mode

This separation helped clarify the architecture.

The Service Worker provides offline access to the application itself, while localStorage provides local persistence for user data.

They solve related but different problems.

12.7 Security Must Be Considered Even Without a Backend

Another important lesson was that removing a backend does not eliminate security concerns.

It changes the threat model.

There is no authentication system to protect and no server-side database to secure, but the application still processes user-controlled data.

That means concerns such as HTML injection, unsafe rendering, malformed imported data, and local storage limitations still matter.

For example, user-entered content should not be inserted into the DOM as trusted HTML.

element.innerHTML = escapeHtml(userInput);
Enter fullscreen mode Exit fullscreen mode

Similarly, imported backup files should be treated as untrusted input rather than assuming they were generated by the application.

The broader lesson is:

Security is not a feature that belongs only to backend applications.

Even a client-only application needs a clearly defined security model.

12.8 Features Should Follow the Data Model

Another lesson came from adding features incrementally.

It is easy to implement a feature visually first and think about the underlying data later.

For example, note colors initially look like a simple UI concern:

Click color
    ↓
Change card background
Enter fullscreen mode Exit fullscreen mode

But the selected color is actually part of the note's persistent state.

The real flow is:

User selects color
        ↓
Note state is updated
        ↓
State is persisted
        ↓
Card is rendered using saved color
Enter fullscreen mode Exit fullscreen mode

The same applies to:

  • Note ordering
  • Creation dates
  • Update dates
  • Theme preference
  • Note identifiers

A feature is usually more robust when its relationship with the data model is defined before its UI implementation.

12.9 Native Features Often Require Careful Fallbacks

Another practical lesson was that browser APIs are not always equally available or equally capable across platforms.

The Web Share API is a good example.

On supported mobile browsers, a backup file can be shared through the native sharing interface. On browsers where the API is unavailable, the application can fall back to a normal file download.

Conceptually:

Backup
  │
  ├── Web Share API available
  │       ↓
  │   Native sharing
  │
  └── Otherwise
          ↓
      File download
Enter fullscreen mode Exit fullscreen mode

This pattern is useful for progressive enhancement.

Instead of designing the application around the assumption that every modern API exists everywhere, the application can use the API when available and provide a simpler fallback when it is not.

That makes the application more resilient without requiring additional dependencies.

12.10 Small Projects Are Good Places to Experiment with Architecture

Perhaps the most valuable lesson from MyNotes is that a small project is an excellent environment for experimenting with architectural ideas.

Because the application has a relatively limited scope, it was possible to explore:

  • Local-first data management
  • Offline support
  • Progressive Web App architecture
  • Native browser APIs
  • Pointer-based interactions
  • Client-side validation
  • Data export and import
  • Responsive UI
  • State synchronization

without the overhead of a large production system.

This makes small applications useful engineering exercises.

They provide enough complexity to expose real architectural problems while remaining small enough to allow experimentation and refactoring.

12.11 The Main Takeaway

The biggest lesson from building MyNotes was not a specific API or JavaScript technique.

It was learning to evaluate the problem before choosing the technology.

A requirement such as:

"The user should be able to save notes."

does not automatically imply:

Frontend
   ↓
REST API
   ↓
Backend
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

For this particular application, a different architecture was sufficient:

                    ┌───────────────┐
                    │   Browser     │
                    │               │
                    │ HTML / CSS    │
                    │ JavaScript    │
                    └───────┬───────┘
                            │
              ┌─────────────┼─────────────┐
              │             │             │
              ▼             ▼             ▼
         localStorage   Service Worker   Web APIs
              │             │             │
              ▼             ▼             ▼
           Notes          Offline       Browser
            Data          Resources    Features
Enter fullscreen mode Exit fullscreen mode

The correct architecture depends on the actual requirements.

For a personal, local-first notes application, eliminating the backend can reduce complexity considerably.

For a multi-user collaborative application, the same decision would obviously not be appropriate.

The important part is not choosing the simplest architecture in every situation.

It is choosing the simplest architecture that satisfies the requirements.

That is probably the most useful lesson I took away from building MyNotes.


13. Future Improvements

MyNotes currently provides the core functionality I wanted from a lightweight, local-first notes application. However, the current architecture also leaves several interesting directions for future development.

The goal would not be to add features simply for the sake of making the application larger. Any future change should solve a real limitation or improve the reliability, usability, or scalability of the application.

13.1 Moving from localStorage to IndexedDB

The most obvious technical improvement would be replacing localStorage with IndexedDB.

localStorage works well for the current application because the amount of data is relatively small and the data model is simple.

However, it has several limitations:

  • Storage is synchronous.
  • Data is stored as strings.
  • The entire notes collection is serialized when it is saved.
  • Querying individual records is not as flexible.
  • It is not designed for larger datasets.

IndexedDB provides a more suitable storage model for applications that need to manage larger amounts of structured client-side data.

The architecture could eventually move from:

Application State
       ↓
JSON.stringify()
       ↓
localStorage
Enter fullscreen mode Exit fullscreen mode

to something closer to:

Application State
       ↓
IndexedDB
       ↓
Object Store
       ↓
Individual Records
Enter fullscreen mode Exit fullscreen mode

This would also make it easier to introduce more advanced features such as larger attachments, richer metadata, or more complex queries.

However, this would also increase implementation complexity.

For the current scope of MyNotes, localStorage remains a reasonable trade-off. IndexedDB would become more attractive as the application's data model grows.

13.2 Versioned Backup and Data Migration

The current backup system uses JSON because it is simple, portable, and easy to inspect.

A future version could introduce an explicit backup schema version.

For example:

{
  "version": 2,
  "exportedAt": "2026-09-02T10:30:00.000Z",
  "notes": []
}
Enter fullscreen mode Exit fullscreen mode

This would make it possible to evolve the internal data structure without breaking older backups.

A future restore process could then follow a migration pipeline:

Backup File
    ↓
Read JSON
    ↓
Validate Format
    ↓
Check Version
    ↓
Migrate if Necessary
    ↓
Normalize Data
    ↓
Import
Enter fullscreen mode Exit fullscreen mode

For example, if a future version adds a new property to every note, an older backup could be transformed automatically into the new format.

This becomes increasingly important when an application has a long-lived data model.

13.3 Optional Client-Side Encryption

Because MyNotes stores data locally, another possible improvement would be optional client-side encryption.

The current security model assumes that the browser's local storage environment is an acceptable place for the user's notes.

For users who want additional protection, sensitive notes could potentially be encrypted before being stored.

Conceptually:

User Data
    ↓
Encryption
    ↓
Encrypted Data
    ↓
localStorage / IndexedDB
Enter fullscreen mode Exit fullscreen mode

The Web Crypto API could provide the cryptographic primitives required for such a design.

However, encryption introduces significant complexity.

A secure implementation would need to carefully consider:

  • Key generation
  • Password-based key derivation
  • Key storage
  • Encryption modes
  • Authentication tags
  • Password recovery
  • Backup encryption
  • Loss of encryption keys

Therefore, encryption should not be treated as a simple feature that can be added by calling a single API.

If implemented in the future, it should be designed as a complete security architecture rather than as an isolated UI option.

13.4 Improved Backup Management

The current backup functionality focuses on exporting and restoring the complete notes collection.

A future version could provide more control over backups.

Possible improvements include:

  • Exporting selected notes
  • Exporting notes by date range
  • Importing without immediately replacing existing data
  • Merging imported notes with existing notes
  • Detecting duplicate note IDs
  • Showing an import preview
  • Keeping multiple local backup snapshots
  • Supporting different export formats

For example, instead of immediately importing a backup, the application could first display a summary:

Import Preview
──────────────
Notes found:       42
New notes:         35
Existing notes:     7
Conflicts:          2

[Cancel]   [Import]
Enter fullscreen mode Exit fullscreen mode

This would make restore operations safer and more transparent.

13.5 Better Conflict Handling

Conflict handling becomes particularly interesting if backup merging or synchronization is introduced.

Suppose a note exists in two different datasets:

Local Version
─────────────
Title: Project Ideas
Updated: 10:30
Content: Version A


Imported Version
────────────────
Title: Project Ideas
Updated: 10:45
Content: Version B
Enter fullscreen mode Exit fullscreen mode

Simply replacing one version with the other may result in data loss.

A more advanced system could compare:

  • Note ID
  • Creation time
  • Update time
  • Content
  • Color
  • Other metadata

and determine whether the records can be merged automatically or require user intervention.

This would become especially important if the application eventually supports synchronization between devices.

13.6 Optional Cross-Device Synchronization

The current architecture intentionally does not use a backend.

That is an important part of the project's design.

However, one possible future direction would be an optional synchronization layer.

The architecture could eventually become:

                    ┌───────────────┐
                    │   MyNotes UI  │
                    └───────┬───────┘
                            │
                 ┌──────────┴──────────┐
                 │                     │
                 ▼                     ▼
          Local Storage           Sync Layer
                 │                     │
                 │                     ▼
                 │                  Server
                 │                     │
                 └──────────┬──────────┘
                            ▼
                       Local State
Enter fullscreen mode Exit fullscreen mode

This would allow the application to preserve its local-first behavior while optionally synchronizing data between devices.

However, synchronization would fundamentally change the complexity of the application.

It would introduce new requirements such as:

  • Authentication
  • Remote storage
  • Conflict resolution
  • Network error handling
  • Synchronization state
  • Data consistency
  • Server-side security
  • Privacy considerations

For this reason, synchronization should not be added simply because modern applications are expected to have accounts and cloud storage.

It should only be introduced if cross-device access becomes an actual requirement.

13.7 Improved Offline Synchronization

If a remote synchronization layer were eventually introduced, the Service Worker could become part of a more advanced offline architecture.

The application could continue working while disconnected:

Offline
   ↓
User edits note
   ↓
Local state updated
   ↓
Change queued
   ↓
Network becomes available
   ↓
Synchronization
   ↓
Server updated
Enter fullscreen mode Exit fullscreen mode

This would move the application closer to a true local-first synchronization model.

However, this would require a much more sophisticated data layer than the current localStorage implementation.

The current architecture deliberately avoids this complexity.

13.8 Richer Note Content

Another possible direction would be supporting richer note content.

Currently, notes are based primarily on text fields.

Future versions could potentially support:

  • Markdown
  • Checklists
  • Tags
  • Categories
  • Links
  • Attachments
  • Images
  • Code snippets
  • Pinned notes
  • Archived notes

However, richer content would also change the application's security and storage requirements.

For example, supporting user-generated Markdown or HTML would require careful sanitization before rendering content.

This demonstrates an important principle:

A new UI feature can create new architectural and security requirements.

Therefore, richer content should be introduced only together with an appropriate data and rendering model.

13.9 Better Search and Filtering

The current search functionality is intentionally simple and appropriate for a small collection of notes.

As the number of notes grows, more advanced search capabilities could become useful.

Possible improvements include:

  • Tag-based filtering
  • Date filtering
  • Color filtering
  • Archived note filtering
  • Search operators
  • Exact phrase matching
  • Fuzzy search
  • Highlighting matching text

For example:

tag:work meeting
Enter fullscreen mode Exit fullscreen mode

could search for notes tagged with work containing the word meeting.

At that point, a more structured storage system such as IndexedDB would become increasingly useful because search requirements would no longer be limited to scanning a small in-memory array.

13.10 Improved Accessibility

Accessibility is another area that could be expanded over time.

The current interface is designed to be responsive and usable across desktop and mobile devices, but more advanced accessibility support could include:

  • Improved keyboard navigation
  • More explicit focus management
  • Better screen reader announcements
  • ARIA attributes where appropriate
  • Keyboard-based note reordering
  • Reduced-motion support
  • Improved contrast validation
  • More descriptive interactive labels

Drag-and-drop is a particularly interesting example.

A pointer-based drag interaction is useful for mouse and touch users, but an accessible implementation should not make pointer interaction the only way to reorder notes.

A future implementation could provide keyboard controls such as:

Move note up
Move note down
Move note to top
Move note to bottom
Enter fullscreen mode Exit fullscreen mode

This would provide an alternative interaction model rather than assuming that every user can perform pointer-based dragging.

13.11 Better PWA Integration

The current PWA implementation provides the basic capabilities needed to install and run MyNotes as a standalone application.

Future improvements could explore deeper platform integration, depending on browser support.

Potential areas include:

  • More advanced caching strategies
  • Better update handling
  • Application shortcuts
  • Share Target API
  • File handling integration
  • Background synchronization where appropriate
  • Improved installation experience

For example, a future version could potentially allow a user to share text from another application directly into MyNotes.

The data flow could look like:

Other Application
       ↓
System Share
       ↓
MyNotes
       ↓
New Note
Enter fullscreen mode Exit fullscreen mode

This would make the PWA behave more like a native application while still being implemented with standard web technologies.

13.12 Maintaining a Small Dependency Footprint

Future development does not necessarily mean adding more libraries.

One goal I would like to preserve is the relatively small dependency footprint of the project.

If a future requirement can be implemented reliably using a native browser API, that option should be evaluated before introducing another dependency.

The decision process could remain:

New Requirement
      ↓
Can the browser provide it?
      │
   ┌──┴──┐
  Yes    No
   │      │
   ▼      ▼
Native   Evaluate
API      Dependency
Enter fullscreen mode Exit fullscreen mode

This approach keeps the application easier to understand, deploy, and maintain.

It also keeps the project aligned with its original goal: demonstrating what can be built with the web platform itself.

13.13 The Principle Behind Future Development

The most important future improvement is not a particular feature.

It is maintaining the same architectural discipline as the project evolves.

Every new feature should be evaluated in terms of:

Requirement
    ↓
User Value
    ↓
Data Model
    ↓
Architecture
    ↓
Security
    ↓
Complexity
    ↓
Implementation
Enter fullscreen mode Exit fullscreen mode

This prevents the application from accumulating features without a coherent architecture.

For example, adding cloud synchronization is not simply a matter of adding an API call. It changes persistence, authentication, conflict handling, security, privacy, and offline behavior.

Similarly, adding rich text is not simply a matter of adding an editor. It changes the data model, rendering pipeline, storage requirements, and security model.

The objective should therefore not be to make MyNotes as feature-rich as possible.

The objective should be to make each new capability worth the complexity it introduces.

13.14 A Possible Evolution Path

If the application continues to evolve, a reasonable progression could be:

Current
  │
  ├── localStorage
  ├── Service Worker
  ├── PWA
  └── JSON Backup
       │
       ▼
Next Stage
  │
  ├── IndexedDB
  ├── Versioned Data
  ├── Better Import / Export
  └── Improved Accessibility
       │
       ▼
Advanced Stage
  │
  ├── Optional Encryption
  ├── Richer Search
  ├── Rich Note Content
  └── Advanced PWA Integration
       │
       ▼
Optional Cloud Layer
  │
  ├── Authentication
  ├── Synchronization
  ├── Conflict Resolution
  └── Remote Backup
Enter fullscreen mode Exit fullscreen mode

Not every stage needs to happen.

In fact, one of the advantages of the current architecture is that the application is already useful without implementing the advanced stages.

Future development should therefore remain driven by actual requirements rather than by the desire to make the technology stack more complicated.

The current version of MyNotes demonstrates that a useful application can be built entirely around the browser platform. The future challenge is to extend that capability while preserving the simplicity, local-first behavior, and maintainability that shaped the original design.


14. Conclusion

Building MyNotes started with a relatively simple goal: create a practical notes application that could run entirely in the browser without requiring a backend.

What made the project interesting was not the basic note-taking functionality itself, but the engineering decisions required to make that approach work well.

The application combines several capabilities provided directly by the modern web platform:

HTML
CSS
Vanilla JavaScript
      │
      ├── localStorage
      ├── Pointer Events
      ├── File APIs
      ├── Web Share API
      ├── Service Worker
      ├── Web App Manifest
      └── Other Web APIs
Enter fullscreen mode Exit fullscreen mode

Together, these technologies are enough to build an application that is persistent, responsive, offline-capable, installable, and usable across different device types.

14.1 No Backend Does Not Mean No Architecture

One of the main conclusions from this project is that removing a backend does not eliminate architectural concerns.

It simply changes the architecture.

Instead of designing around:

Client
  ↓
API
  ↓
Server
  ↓
Database
Enter fullscreen mode Exit fullscreen mode

MyNotes is designed around:

Browser
  │
  ├── Application State
  │
  ├── localStorage
  │
  ├── Service Worker
  │
  ├── Cache
  │
  └── Browser APIs
Enter fullscreen mode Exit fullscreen mode

This architecture is appropriate because the application's requirements are local-first.

There is no need for user accounts, server-side processing, multi-user collaboration, or cross-device synchronization.

Adding a backend simply because most applications have one would introduce complexity without solving an actual requirement.

That is an important architectural principle:

Technology should follow requirements, not the other way around.

14.2 The Browser Is Already a Powerful Application Platform

Another conclusion is that modern browsers provide a surprisingly capable runtime for application development.

With the right combination of APIs, a browser application can provide functionality that traditionally required additional infrastructure.

For MyNotes, the browser provides:

  • Persistent local data
  • File import and export
  • Native sharing capabilities
  • Offline resource caching
  • Installable application behavior
  • Pointer-based interaction
  • Responsive rendering
  • Cryptographically strong identifiers

This does not mean that every application should be built without a backend or framework.

Instead, it means that developers should understand the capabilities of the platform before deciding what additional infrastructure is necessary.

Sometimes the simplest solution is already available.

14.3 Local-First Is a Trade-Off

The local-first architecture provides several benefits:

No server
    ↓
No network dependency
    ↓
Fast local interaction
    ↓
Simple deployment
    ↓
User data stays on the device
Enter fullscreen mode Exit fullscreen mode

But these benefits come with limitations.

The application does not automatically provide:

  • Cross-device synchronization
  • Centralized backups
  • User accounts
  • Server-side recovery
  • Collaborative editing

These are not bugs in the architecture.

They are trade-offs resulting from the chosen requirements.

If those requirements change in the future, the architecture may need to evolve as well.

14.4 Complexity Should Be Justified

Throughout the project, one recurring principle became increasingly clear:

Every additional layer should solve a real problem.

A dependency, framework, backend service, database, or synchronization layer can be valuable when the application needs it.

But every additional component also introduces:

  • More code
  • More maintenance
  • More failure points
  • More security considerations
  • More deployment requirements
  • More architectural complexity

For a relatively small application, keeping the technology stack focused can be a significant advantage.

The objective is not to use as many technologies as possible.

The objective is to use the right technologies for the problem.

14.5 What MyNotes Demonstrates

MyNotes is intentionally not a replacement for large-scale productivity applications.

It is a practical demonstration of what can be achieved with the browser platform itself.

The project brings together several concepts that are often discussed independently:

Client-Side State
       +
Local Persistence
       +
Responsive UI
       +
Pointer Interaction
       +
File Handling
       +
Offline Support
       +
PWA
       +
Security Considerations
Enter fullscreen mode Exit fullscreen mode

The interesting part is how these pieces interact.

For example, adding offline support affects resource caching.

Adding local persistence affects data management.

Adding backup and restore affects validation.

Adding drag-and-drop affects state synchronization.

Adding richer user-controlled content affects security.

This is where a small application becomes a useful engineering exercise.

14.6 The Final Takeaway

A modern web application does not always need a large technology stack to be useful.

Sometimes a carefully designed combination of HTML, CSS, JavaScript, and native browser APIs is enough.

MyNotes started as a simple notes application, but developing it demonstrated something more valuable: good software architecture is primarily about making appropriate decisions for the problem at hand.

The absence of a backend was not a shortcut.

It was a deliberate architectural decision based on the application's requirements.

The use of vanilla JavaScript was not a rejection of modern frameworks.

It was a conscious choice to keep the project lightweight and make the underlying browser APIs visible.

And the use of local storage and offline capabilities was not simply about adding features.

It was about designing the application around the user's data and interaction model.

Ultimately, the project reinforced a principle that applies to applications of every size:

Start with the requirements.
        ↓
Understand the constraints.
        ↓
Evaluate the platform.
        ↓
Choose the simplest suitable architecture.
        ↓
Add complexity only when it provides real value.
Enter fullscreen mode Exit fullscreen mode

That is the approach behind MyNotes, and it is also the main lesson I would take from building it.

Top comments (0)