Local‑First Writing App Clone

A minimal FocusWrite‑inspired editor with local persistence

Welcome to another official UVF IT guide. In the next 45 minutes you’ll spin up a tiny writing app that stores drafts locally, mirroring the core of FocusWrite without any cloud services.

We’ll use Python 3, Flask, and SQLite – all free‑tier, open‑source tools – to deliver a clean UI, a save‑on‑type backend, and a simple focus mode toggle.

Before you start

Component Overview
LOCAL STACK HTTP SQL HTML/JS UI Flask API SQLite DB
1

Phase 1: Scaffold the project

Create a folder, a virtual environment, and install Flask. This gives us a clean, isolated workspace.

mkdir focuswrite_clone && cd focuswrite_clone
python3 -m venv venv
source venv/bin/activate  # on Windows use `venv\Scripts\activate`
pip install --upgrade pip
pip install Flask
2

Phase 2: Define the data model

We’ll use SQLite with a single drafts table to store title, content, and a timestamp.

init_db.py

import sqlite3
import os

DB_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'drafts.db')

conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS drafts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
print('Database initialized at', DB_PATH)
3

Phase 3: Build the Flask API & UI

In this step we spin up a tiny Flask server that serves a single HTML page and provides an endpoint for saving drafts. The server will store each save in a local SQLite database, keeping everything on the user's machine. We also expose a static folder so the browser can request the front‑end assets we will add later. This keeps the back‑end minimal yet functional for a local‑first writing app.

app.py

import os
from flask import Flask, request, send_from_directory, jsonify
import sqlite3

app = Flask(__name__, static_folder='static')
DB_PATH = 'drafts.db'

# Ensure the SQLite DB and table exist
if not os.path.exists(DB_PATH):
    conn = sqlite3.connect(DB_PATH)
    conn.execute('CREATE TABLE drafts (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)')
    conn.commit()
    conn.close()

@app.route('/')
def index():
    # Serve the main editor page
    return send_from_directory(app.static_folder, 'index.html')

@app.route('/save', methods=['POST'])
def save():
    data = request.get_json()
    content = data.get('content', '')
    conn = sqlite3.connect(DB_PATH)
    conn.execute('INSERT INTO drafts (content) VALUES (?)', (content,))
    conn.commit()
    conn.close()
    return jsonify({'status': 'saved'}), 200

if __name__ == '__main__':
    app.run(debug=True)
4

Phase 4: Add the static front‑end

Now we create the HTML UI that the Flask server will deliver. It contains a large textarea for writing and a button that toggles a "focus mode" class, which hides the button and any surrounding chrome. JavaScript listens for input events, debounces them, and POSTs the current content to the /save endpoint so every keystroke is persisted locally.

static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Local‑First Writer</title>
    <style>
        body {font-family: Arial, sans-serif; margin:0; padding:0; height:100vh; display:flex; flex-direction:column;}
        #editor {flex:1; width:100%; padding:1rem; box-sizing:border-box; font-size:1.2rem;}
        .focus-mode #focusBtn {display:none;}
        .focus-mode textarea {background:#fdfdfd;}
    </style>
</head>
<body>
    <button id="focusBtn">Focus Mode</button>
    <textarea id="editor" placeholder="Start typing your masterpiece…"></textarea>
    <script>
        const editor = document.getElementById('editor');
        const btn = document.getElementById('focusBtn');
        btn.addEventListener('click', () => document.body.classList.toggle('focus-mode'));
        let timeout;
        editor.addEventListener('input', () => {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                fetch('/save', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({content: editor.value})
                });
            }, 300);
        });
    </script>
</body>
</html>
5

Phase 5: Run and verify

With the back‑end and front‑end in place, start the Flask development server and open the app in a browser. Begin typing; the JavaScript will debounce and POST each change, and the server will insert a new row into the SQLite file. You can confirm persistence by checking the drafts.db file size or querying the table directly.

python app.py

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorspython app.py shows *Running on http://127.0.0.1:5000/*
Browser loads editor UIOpen http://127.0.0.1:5000/ and see the textarea
Typing creates a row in SQLiteAfter typing, sqlite3 drafts.db "SELECT COUNT(*) FROM drafts;" returns a number >0

What’s next?

Add a list view to load previous drafts, implement markdown preview, or sync the DB to a cloud folder for true multi‑device access. Happy hacking!