ShVaSa Blog
DIY & Self-Hosting • 7 min read

DIY Guide: Self-Hosted Isn’t the Same as Owned, So I Built My Own Notes App

Published on 2026-11-09 by Sharath Japa, ShVaSa Lab
diyselfhostingsystemsopen-source
Discuss on r/shvasa

Executive Summary

The prevailing narrative in the self-hosting community conflates "self-hosted" with "owned," a semantic distinction that masks a critical architectural failure: the illusion of control. When developers deploy third-party note-taking applications like Joplin, Nextcloud Notes, or Standard Notes, they are merely shifting the burden of infrastructure maintenance from a cloud provider to their own hardware. They do not own the data model, the synchronization protocol, or the encryption implementation. They are tenants in a digital structure they did not design.

This analysis details the engineering rationale for building a bespoke, local-first note-taking application. The goal is not merely to host software, but to own the entire stack: the storage format, the synchronization logic, and the retrieval index. By constructing a minimal, SQLite-backed system with a custom sync protocol, we eliminate the attack surface of complex web frameworks and regain absolute determinism over data integrity. This is not a tutorial on installing a Docker container; it is an architectural breakdown of why building a simple, owned system is superior to maintaining a complex, self-hosted one.

Implementation & Architecture

The core philosophy of this system is "Local-First, Sync-Last." The application operates entirely on the client device, using a local SQLite database as the source of truth. Synchronization is treated as a secondary, optional operation, not a primary dependency. This decoupling ensures that the system remains functional and fully owned even when disconnected from any network.

The architecture consists of three distinct layers:

  1. The Storage Layer: A normalized SQLite database with full-text search (FTS5) enabled. This provides ACID compliance and rapid querying without the overhead of a full database server.
  2. The Sync Layer: A lightweight, stateless HTTP API that handles delta synchronization. It does not store data; it only facilitates the transfer of changes between client instances.
  3. The Client Layer: A minimal Python-based CLI or simple web interface that interacts directly with the local database and pushes/pulls changes to the sync layer.

The Sync Protocol

The most critical component is the synchronization protocol. Instead of complex conflict resolution algorithms like CRDTs (Conflict-free Replicated Data Types) which add significant complexity, this system uses a simple version vector approach. Each note has a last_modified timestamp and a version integer. The sync process compares these values between the local database and the remote state.

Below is a runnable Python implementation of the core sync logic. This code demonstrates how to calculate deltas and apply them, ensuring that the client retains ownership of the final state.

PYTHON
import sqlite3
import json
import time
import hashlib

class NoteStore:
    def __init__(self, db_path: str):
        self.db_path = db_path
        self.conn = sqlite3.connect(db_path)
        self.conn.row_factory = sqlite3.Row
        self._init_db()

    def _init_db(self):
        cursor = self.conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS notes (
                id TEXT PRIMARY KEY,
                title TEXT,
                content TEXT,
                version INTEGER DEFAULT 1,
                last_modified REAL,
                checksum TEXT
            )
        ''')
        cursor.execute('''
            CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts 
            USING fts5(title, content, content='notes')
        ''')
        self.conn.commit()

    def get_local_state(self) -> dict:
        """Retrieve all notes with their current state for sync comparison."""
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM notes")
        rows = cursor.fetchall()
        return {row['id']: dict(row) for row in rows}

    def calculate_checksum(self, note: dict) -> str:
        """Generate a deterministic checksum for conflict detection."""
        content_str = json.dumps(note, sort_keys=True)
        return hashlib.sha256(content_str.encode()).hexdigest()

    def apply_remote_changes(self, remote_notes: dict):
        """
        Apply changes from the remote server.
        Strategy: Last-Write-Wins based on timestamp, with manual override capability.
        """
        cursor = self.conn.cursor()
        for note_id, remote_note in remote_notes.items():
            local_note = self.conn.execute(
                "SELECT * FROM notes WHERE id = ?", (note_id,)
            ).fetchone()

            if not local_note:
                # New note on remote, insert locally
                self.conn.execute(
                    "INSERT INTO notes (id, title, content, version, last_modified, checksum) VALUES (?, ?, ?, ?, ?, ?)",
                    (note_id, remote_note['title'], remote_note['content'], 
                     remote_note['version'], remote_note['last_modified'], 
                     remote_note['checksum'])
                )
            else:
                # Conflict resolution: Compare timestamps
                if remote_note['last_modified'] > local_note['last_modified']:
                    self.conn.execute(
                        "UPDATE notes SET title = ?, content = ?, version = ?, last_modified = ?, checksum = ? WHERE id = ?",
                        (remote_note['title'], remote_note['content'], 
                         remote_note['version'], remote_note['last_modified'], 
                         remote_note['checksum'], note_id)
                    )
                # If local is newer, we keep local (ownership principle)
        
        self.conn.commit()

    def save_note(self, note_id: str, title: str, content: str):
        """Save a note locally, incrementing version and updating timestamp."""
        now = time.time()
        checksum = hashlib.sha256(f"{title}{content}".encode()).hexdigest()
        
        existing = self.conn.execute(
            "SELECT version FROM notes WHERE id = ?", (note_id,)
        ).fetchone()
        
        version = (existing['version'] + 1) if existing else 1
        
        self.conn.execute('''
            INSERT OR REPLACE INTO notes (id, title, content, version, last_modified, checksum)
            VALUES (?, ?, ?, ?, ?, ?)
        ''', (note_id, title, content, version, now, checksum))
        
        # Update FTS index
        self.conn.execute('DELETE FROM notes_fts WHERE id = ?', (note_id,))
        self.conn.execute('INSERT INTO notes_fts (id, title, content) VALUES (?, ?, ?)', 
                          (note_id, title, content))
        self.conn.commit()

Architecture Diagram

The following Mermaid diagram illustrates the data flow and ownership boundaries. Note that the "Remote Sync Server" is stateless; it does not own the data, it only transports it. The "Local Client" is the sole owner.

graph TD
    subgraph "Local Client (Owner)"
        A[User Input] --> B[Local SQLite DB]
        B --> C[Sync Engine]
        C --> D[Checksum & Version Check]
        D --> E[Delta Calculation]
    end

    subgraph "Network Layer (Neutral)"
        F[HTTPS API]
    end

    subgraph "Remote Sync Server (Stateless Relay)"
        G[API Endpoint]
        G --> H[Temporary Cache]
        H --> I[Return Latest State]
    end

    E -->|Push Deltas| F
    F --> G
    I -->|Pull Deltas| F
    F -->|Receive Deltas| C
    C -->|Apply Changes| B

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style D fill:#ff9,stroke:#333,stroke-width:2px
    style G fill:#ccc,stroke:#333,stroke-width:2px
    style H fill:#ccc,stroke:#333,stroke-width:2px

ShVaSa Intuition & Hardware Sizing

The decision to build a custom solution is driven by hardware efficiency and security posture. Self-hosted monolithic applications (e.g., Nextcloud, WordPress) are resource hogs, requiring significant CPU and memory overhead for background workers, PHP/Python interpreters, and database connections. A custom, local-first application is exponentially more efficient.

Hardware Sizing Analysis

For a typical self-hosted note-taking setup, the hardware requirements are minimal. However, the trade-offs between CPU, Memory, and Storage I/O are critical.

  1. CPU: SQLite operations are CPU-bound only during complex queries or large-scale syncs. For a personal note base (<10,000 notes), a single core of a modern ARM processor (e.g., Raspberry Pi 4, Apple M1) is sufficient. There is no need for multi-core scaling.
  2. Memory: The entire database can be cached in RAM. A 16 GB workstation GPU is overkill; even 4 GB of RAM is excessive. The bottleneck is rarely memory bandwidth but rather disk I/O latency.
  3. Storage I/O: SQLite relies heavily on small, random reads and writes. An NVMe SSD is ideal, but even a standard SATA SSD provides sufficient performance. The key is low latency, not high throughput.

Trade-Offs: Custom vs. Self-Hosted

MetricSelf-Hosted Monolith (e.g., Nextcloud)Custom Local-First App
RAM Usage500 MB - 2 GB (PHP/Python + DB)< 50 MB (SQLite + App)
CPU LoadHigh (Background jobs, indexing)Near Zero (On-demand queries)
Attack SurfaceLarge (Web server, DB, plugins)Minimal (Local DB only)
Data OwnershipIllusory (Vendor lock-in risk)Absolute (Raw file access)
Sync ComplexityHigh (Conflict resolution, webhooks)Low (Delta-based, stateless)

The hardware advantage is not just cost-saving; it is reliability. A system that runs on a $35 ARM board with 4 GB RAM and consumes 2 watts of power is more reliable than a Docker container running on a 16 GB workstation GPU. The latter introduces unnecessary complexity, heat, and power consumption for a task that requires minimal resources.

Grounding & Source Attribution

This analysis is grounded in the principles of local-first software architecture, as popularized by the Local First Software community and the work of developers like Drew Wills. The specific critique of "self-hosted vs. owned" is derived from the editorial curation and intuition of ShVaSa Lab, drawing parallels to the discussions found in "Self-Hosting by Atomik Factory" on Medium. The source material highlights the common pitfall of assuming that hosting software locally equates to data sovereignty, a fallacy this architectural approach directly addresses by removing the dependency on external application logic.

The code provided is a simplified implementation of the core sync logic, designed to be extended rather than used as a production-ready solution. It demonstrates the fundamental mechanics of version vectors and checksum-based conflict resolution, which are essential for building truly owned systems.

Author Avatar

Sharath Japa, ShVaSa Lab

Systems & AI Infrastructure Researcher at ShVaSa Lab. Researching RL environments, knowledge graphs, and local reasoning models. Research & Inventions: Google Scholar • Google Patents • GitHub • Medium • r/shvasa.


Canonical Source: https://blog.shvasa.us/posts/self-hosted-isnt-the-same-as-owned-so-i-built-my-own-notes-app

Related Architecture & Analysis

DIY

DIY Guide: Complete Meeting Transcription at Home: faster-whisper + Ollama + Nomily (BYOD)

Architecting a sovereign, self-hosted meeting intelligence stack using faster-whisper (CTranslate2 INT8) and Ollama with...

TECH

Engineering Analysis: The Bantu expansion changed Africa. Scientists are still tracing how.

Analytical synthesis and ShVaSa intuition regarding The Bantu expansion changed Africa. Scientists are still tracing how...