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 │
└────────────┘ └────────────┘ └────────────┘
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:
- Load notes from local storage.
- Maintain the current application state.
- Render the notes.
- Respond to user interactions.
- Update the state.
- 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 = [];
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;
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
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
MyNotes uses a different model:
Browser
│
▼
JavaScript
│
▼
localStorage
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";
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));
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";
The selected application theme is stored separately:
const THEME_KEY = "notes_app_theme";
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: "..."
}
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"
}
]
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
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
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 │
└────────────────┘ └─────────────────┘
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
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"
}
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
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
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
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
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
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();
}
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;
}
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)
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];
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());
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.
The application also updates the note counter to reflect the number of visible results:
3 / 12 Notes
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);
}
});
}
Date-based sorting uses the timestamps stored with each note:
function compareDates(dateA, dateB)
{
return new Date(dateA).getTime() - new Date(dateB).getTime();
}
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")
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;
}
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();
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.
...
}
Otherwise:
/*
* When a search is active, reorder only the visible notes.
*/
const visibleSet = new Set(visibleIds);
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
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>
`
: "";
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);
});
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,
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);
The actual drag does not begin until the pointer has moved at least six pixels:
if (!dragState.dragging && distance < 6)
{
return;
}
This small threshold separates a simple pointer interaction from an intentional drag operation.
Once the threshold is exceeded, the application calls:
beginActualDrag();
8.4 Maintaining Drag State
The drag operation is managed through a single state object:
let dragState = null;
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
};
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);
}
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");
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`;
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);
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";
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);
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`;
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
);
For each column, its center point is calculated:
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
The distance between the pointer and each card is then evaluated.
On desktop layouts, horizontal position receives more importance:
score = horizontalDistance + verticalDistance * 1.5;
On mobile layouts, vertical position becomes more important:
score = verticalDistance + horizontalDistance * 0.25;
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;
For mobile layouts, the vertical position is used:
insertBefore = clientY < targetRect.top + targetRect.height / 2;
The placeholder is then moved in the DOM:
notesContainer.insertBefore(placeholder, targetColumn);
or:
notesContainer.insertBefore(placeholder, next);
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);
The original Bootstrap column is placed where the placeholder currently resides:
if (placeholder)
{
notesContainer.insertBefore(state.column, placeholder);
placeholder.remove();
}
The card is then moved back into its original column:
state.column.appendChild(card);
Finally, all temporary inline styles are removed:
resetDraggedCardStyles(card);
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 = [];
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();
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);
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;
}
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)
);
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();
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
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";
The card click handler checks this value:
if (card.dataset.dragged === "true")
{
delete card.dataset.dragged;
return;
}
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
-
localStoragepersistence - 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);
}
Components can then reference these variables instead of hard-coding their colors:
body {
background-color: var(--app-bg);
color: var(--text);
}
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);
}
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;
}
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);
}
works in both themes.
In light mode:
--surface → light surface color
--text → dark text color
In dark mode:
--surface → dark surface color
--text → light text color
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">
or:
<html data-theme="light">
JavaScript can change the active theme by modifying this attribute:
document.documentElement.dataset.theme = theme;
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";
When the user selects a different theme, the application stores the preference:
localStorage.setItem(THEME_KEY, theme);
When the application starts, it reads the previously selected theme:
const savedTheme = localStorage.getItem(THEME_KEY);
The theme can then be applied:
if (savedTheme)
{
document.documentElement.dataset.theme = savedTheme;
}
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 = [];
The second is the visual preference:
theme = "light"
These are intentionally stored separately.
The notes are stored using:
const STORAGE_KEY = "notes_app_data";
while the theme is stored using:
const THEME_KEY = "notes_app_theme";
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);
The operation therefore consists of three steps:
User changes theme
↓
Update data-theme
↓
CSS variables change
↓
Save preference to localStorage
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 = "...";
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?
CSS decides:
What does that theme look like?
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"
}
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;
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
When the application is rendered again:
localStorage
↓
Load notes
↓
Read note.color
↓
Apply color to card
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
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);
}
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
while:
JavaScript state
↓
Persistent application data
For a theme:
theme value
↓
data-theme attribute
↓
CSS variables
↓
visual appearance
For a note color:
note.color
↓
note rendering
↓
card background
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: ...;
}
The existing components could continue using:
background-color: var(--surface);
color: var(--text);
border-color: var(--border);
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-themefor selecting the active theme -
localStoragefor 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
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 = [];
A backup can therefore be created by serializing this array:
const json = JSON.stringify(notes, null, 2);
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
}
]
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" });
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);
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";
The application programmatically triggers the download:
link.click();
After the operation is complete, the temporary object URL can be released:
URL.revokeObjectURL(url);
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`;
For example:
my-notes-backup-2026-08-31-13-24-20.json
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"
>
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();
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
}
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"
}
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.");
}
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
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"
);
}
The application can then validate the complete collection:
if (!importedNotes.every(isValidNote))
{
throw new Error("Invalid note data.");
}
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
};
The application can also apply default values where appropriate.
This creates a controlled transition:
File
↓
Text
↓
JSON.parse()
↓
Validation
↓
Normalization
↓
Application state
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
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;
The new collection is then persisted:
saveNotesToStorage();
Finally, the interface is rendered again:
renderNotes();
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
}
The backup file can then be passed to the native sharing mechanism:
await navigator.share({
title: "MyNotes Backup",
text: "MyNotes backup file",
files: [file]
});
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
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
On a desktop browser without file sharing:
Create backup
↓
Create Blob
↓
Create object URL
↓
Trigger download
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");
}
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:
- The file was successfully read.
- The JSON was successfully parsed.
- The data structure was validated.
- The imported notes were normalized.
- The user explicitly confirmed the operation.
Only then should the application execute:
notes = normalizedNotes;
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
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
The restore pipeline is the reverse:
File
↓
File API
↓
Text
↓
JSON.parse()
↓
Validation
↓
Normalization
↓
User confirmation
↓
notes array
↓
localStorage
↓
UI rendering
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
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 -
Blobfor 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
-
localStoragefor 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
Instead, the architecture is essentially:
Browser
↓
JavaScript Application
↓
localStorage
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)
});
The note is stored locally:
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
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>
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
The important characters are converted into their HTML entity equivalents.
For example:
<script>alert("Hello")</script>
becomes conceptually:
<script>alert("Hello")</script>
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
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);
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
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;
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");
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));
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
and:
Mobile Chrome
↓
MyNotes localStorage
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
If necessary:
JSON Backup
↓
Import
↓
Validation
↓
MyNotes
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();
The contents are then parsed:
const data = JSON.parse(text);
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
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>"
}
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
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
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
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
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
Each layer introduces additional security requirements.
MyNotes intentionally removes most of these layers:
Browser
↓
MyNotes
↓
localStorage
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
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:
-
localStorageis 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
localStoragelimitations - 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
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));
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
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
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
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
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
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);
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
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
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
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
For this particular application, a different architecture was sufficient:
┌───────────────┐
│ Browser │
│ │
│ HTML / CSS │
│ JavaScript │
└───────┬───────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
localStorage Service Worker Web APIs
│ │ │
▼ ▼ ▼
Notes Offline Browser
Data Resources Features
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
to something closer to:
Application State
↓
IndexedDB
↓
Object Store
↓
Individual Records
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": []
}
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
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
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]
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
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
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
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
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
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
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
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
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
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
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
MyNotes is designed around:
Browser
│
├── Application State
│
├── localStorage
│
├── Service Worker
│
├── Cache
│
└── Browser APIs
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
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
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.
That is the approach behind MyNotes, and it is also the main lesson I would take from building it.
Top comments (0)