I built an iPad journal that feels like a real notebook (and saves to my Raspberry Pi)
A Moleskine-style web app for writing with the Apple Pencil or keyboard: pressure-sensitive ink, 3D page turns and a copy of every page on the Pi, no App Store.
I’ve wanted to keep a handwritten journal for a while, but paper notebooks get lost, have no backup, and I end up with three half-used ones. I have an iPad with an Apple Pencil, so the idea was obvious: a digital notebook that feels like a paper one, not yet another notes app full of menus. I built it as a web app served from my Raspberry Pi, and every page I write also ends up on the NAS. Here’s how it works and the decisions behind it.
I wrote it with Claude Code, based on what I kept asking for and testing on the iPad. The code is small: one HTML page, about 700 lines of JavaScript and a 100-line Python server.
1. Why a web app and not an iOS app
The first thing I ruled out was a native app. To install an app you built yourself on an iPad you need Xcode, which only runs on a Mac, and unless you pay for the Apple developer account (99 € a year), the app expires after 7 days. I don’t have a Mac and I’m not paying to write in my own notebook.
A PWA (progressive web app) skips all of that: you open it in Safari, tap “Share > Add to Home Screen” and it gets its own icon, full screen, with no browser bar. All it takes is a manifest.webmanifest and a few tags in the HTML:
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="icon-180.png">
<link rel="manifest" href="manifest.webmanifest">
2. What it needed to feel like a notebook
The rule I set was you can’t do anything you couldn’t do with a real notebook. No infinite undo, no erasing ink, no reordering pages. What it does have:
- A black cover with an elastic band that opens when you tap it, and inside, the “In case of loss, please return to:” page, just like a Moleskine.
- 240 ruled pages at a 13 x 21 cm ratio. In landscape you see the two-page spread; in portrait, a single page.
- 3D page turns: drag with your finger and the page follows. Let go halfway and it falls back into place.
- Three inks: black, blue and pencil. Only pencil can be erased, with an eraser; whatever you write in ink stays.
- The date writes itself in the corner when you start a new page, in whatever ink you’re using.
- A ribbon bookmark that takes you to the last page you wrote on.
- An “Aa” mode for typing, with a handwriting font (Caveat) that wobbles a little on every letter so it doesn’t look printed.
The wobble has a trick to it: with Math.random(), the text would jump around every time the page redraws. I use a seeded pseudo-random generator (mulberry32) instead, so the same text always wobbles the same way.
3. The ink: Pencil pressure and smooth curves
The Apple Pencil reports pressure in pointer events (pointerType === 'pen' and ev.pressure between 0 and 1). That lets the stroke width change as you press harder:
let pr = ev.pointerType === 'pen' ? Math.pow(Math.max(ev.pressure, 0.05), 0.75) : 0.5;
if (trazo.prevPr != null) pr = trazo.prevPr * 0.55 + pr * 0.45; // smoothing
Three details make all the difference:
getCoalescedEvents(): Safari batches fast movements into a single event. If you don’t ask for the coalesced events, quick handwriting comes out as straight segments.- Quadratic curves between midpoints instead of straight lines from point to point. That’s what makes an “o” round instead of a polygon.
- Fingers don’t draw. Your finger turns pages; only the Pencil writes. You also have to block
touchstarton the page, or iOS pops up the magnifier and text selection when you rest the Pencil on it.
Strokes are stored as lists of numbers (x, y, pressure) in coordinates relative to the page, from 0 to 1. That way a page looks the same in landscape, portrait or on another device.
The pencil eraser took a second attempt. Every page has two stacked canvases: one underneath for pencil and one on top for ink. The eraser draws with globalCompositeOperation = 'destination-out' only on the pencil canvas, so if you write in ink over something in pencil and then erase it, the ink is untouched. Just like on paper.
4. Where it’s saved: the iPad first, then the Pi
I didn’t want the journal to depend on a connection. So every page is saved first on the iPad itself, in IndexedDB, and 2.5 seconds after you stop writing it gets uploaded to the Pi. With no network, it retries every 30 seconds.
The Pi’s server is dependency-free Python (http.server) that serves the web files and exposes a tiny API:
| Route | What it does |
|---|---|
GET /api/paginas |
Index with the last-change time of every page |
GET /api/pagina/<n> |
One page as JSON |
PUT /api/pagina/<n> |
Saves the page |
The important part is the PUT. Each page carries a mod field with the time of its last change, and the server rejects with a 409 any version older than the one it already has. Before overwriting, it also keeps the previous one as pagina-N.json.prev and writes to a temp file that it then renames, so a power cut can’t leave a half-written page:
if indice.get(str(n), 0) > mod:
return self.json(409, {'error': 'hay una versión más nueva'})
if os.path.exists(fichero):
os.replace(fichero, fichero + '.prev')
escribir(fichero, json.dumps(pag)) # .tmp + fsync + os.replace
When the app opens it goes the other way: it checks the Pi’s index and downloads any pages that are newer there. So if I ever write on another device, everything is there when I come back to the iPad.
5. Running it on the Pi
On the Pi it’s just another systemd service. It runs as my regular user, but with ProtectSystem=strict and ProtectHome=read-only it can only write to its own data folder:
# /etc/systemd/system/diario.service
[Unit]
Description=Diario (notebook web app, port 8095)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/diario
ExecStart=/usr/bin/python3 server.py
Environment=PORT=8095
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/pi/diario/datos
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now diario
Here’s the catch: at http://192.168.1.50:8095 the journal works, but the service worker never registers, because browsers only allow it over HTTPS (or on localhost). No service worker means no offline mode, and the “app” is just a tab. The fix I already use for Vaultwarden is Tailscale, which puts a valid certificate in front:
sudo tailscale serve --bg --https=8444 http://127.0.0.1:8095
Now the journal lives at https://my-pi.your-net.ts.net:8444, with a proper padlock and reachable from anywhere the iPad has Tailscale, without opening a single port on the router.
The service worker uses a “network first” strategy: with a connection it fetches the latest version of the app; without one it falls back to the cache. When I change something I bump the cache name (diario-v3) so the iPad drops the old one.
6. The NAS copy
The journal folder is now part of my Pi-to-NAS backup script: I just added /home/pi/diario to the list of sources. In the end every page lives in three places: the iPad, the Pi and the NAS, plus the .prev version of each. For a journal, that’s plenty.
What not to do
- Only testing it on the computer. On the desktop everything was perfect; on the iPad, Safari ignored
backface-visibilityduring page turns and both sides of the page showed at once. The fix is giving each side its own transform (translateZ(.5px)), but you only find out on the device. - Serving it over HTTP and assuming offline works. Without HTTPS there’s no service worker, and it doesn’t warn you: it just never registers.
- Storing strokes in pixels. As soon as you rotate the iPad, everything shifts. Relative coordinates from day one.
- Relying only on IndexedDB. If you clear Safari’s data, remove the icon or the iPad runs out of space, whatever is stored in the browser can disappear. That’s what the copy on the Pi is for.
Summary
- Without a Mac or a developer account, a PWA on the home screen is how you get your own app on an iPad.
- For good handwriting: Pencil pressure, coalesced events and curves between midpoints.
- Save on the iPad first, then on the Pi, with simple versioning (
mod, 409 and.prev). - HTTPS with
tailscale serve, or offline mode simply doesn’t exist. - The Pi backs it up to the NAS along with everything else.
It’s the most “pointless” project on my Pi and probably the one I’ll use the most. I actually want to write in it precisely because it isn’t a notes app.
Did it stick?
Three quick questions. Each right answer is worth 10 XP.