A self-hosted handwriting application for e-ink devices. Captures handwritten notes to an infinite canvas, with automatic transcription via Gemini and local file storage.
Inkwell is a web app that runs on a local server and is accessed via a browser on an e-ink tablet (primarily Boox devices). It provides a writing surface for capturing handwritten notes, organised into notebooks containing pages arranged on a 2D infinite canvas.
The application uses boox-rapid-draw for low-latency inking on Boox devices. This Android app intercepts stylus input and renders strokes directly to the e-ink layer, providing instant visual feedback. Inkwell receives the stroke events through standard pointer events and persists them to the filesystem.
It would likely work on other devices and browsers, but the drawing experience without boox-rapid-draw will have noticeable latency on e-ink displays.
The main writing interface. Strokes are rendered with pressure sensitivity using Perfect Freehand.
All pages arranged spatially on a 2D infinite canvas. Pan and zoom to navigate. Drag pages to reposition them.
Grid-based page management. Select multiple pages for bulk operations (tagging, export, move, delete).
Full-text search across transcriptions. Open with the Search button or Cmd+K.
- Notebooks containing pages on a 2D canvas
- Pen, highlighter, and eraser tools with pressure sensitivity
- Multiple stroke widths and colours
- Page backgrounds: plain, lined, grid, dots
- Automatic transcription via Gemini API
- Full-text search across transcriptions
- PDF and PNG export
- Page tags and links
- Inline handwriting links (cross-notebook + external URLs)
- Markdown sync to external directories (e.g. Obsidian vault)
- Offline support via service worker
- Node.js 20+
- A Gemini API key (for transcription)
git clone https://github.com/yourusername/inkwell
cd inkwell
npm installCreate a .env file in the project root:
GEMINI_API_KEY=your_api_key_here
DATA_DIR=/path/to/store/notebooks # optional, defaults to ./data
PORT=3001 # optional, server port
HOST=0.0.0.0 # optional, bind address
AUTO_TRANSCRIBE=true # optional, auto-transcribe pages
TRANSCRIBE_DELAY_MS=30000 # optional, delay before auto-transcribeDevelopment mode (with hot reload):
npm run devThis starts both the server (port 3001) and client dev server (port 5173).
Production build:
npm run build
npm run start --workspace=serverServe the built client files from client/dist using the server's static file serving, or a separate web server.
I run Inkwell on my desktop PC and access it from my Boox tablet over Tailscale. This avoids exposing the server to the internet while allowing access from anywhere on my Tailscale network.
- Install Tailscale on both the server machine and the Boox tablet
- Start Inkwell on the server with
HOST=0.0.0.0 - On the Boox, open Chrome and navigate to
http://<tailscale-ip>:3001
The server binds to 0.0.0.0 by default, so it will be accessible on all network interfaces including Tailscale.
If running the client dev server separately, you'll need to configure Vite to allow external connections and point it at the server. For production, build the client and serve it from the server's static file handler.
- Install boox-rapid-draw on your Boox device
- Configure it to overlay Chrome
- Open Inkwell in Chrome
- Enable the overlay before writing
boox-rapid-draw intercepts stylus input and draws directly to the e-ink framebuffer, bypassing the browser's rendering pipeline. This eliminates the latency that makes writing on e-ink displays frustrating. Inkwell still receives the pointer events and saves the strokes to the server.
All data is stored as JSON files on the filesystem:
/data/inkwell/
├── config.json
└── notebooks/
└── {notebook-id}/
├── meta.json
└── pages/
└── {page-id}/
├── meta.json
├── strokes.json
├── transcription.md
└── thumbnail.png
This makes backup simple (rsync the directory) and the data is easily inspectable and greppable.
The server exposes a REST API at /api and a WebSocket endpoint at /ws for real-time updates. See PLAN.md for the full API specification.
# Run tests
npm test
# Run e2e tests (requires server running)
npm run test:e2e
# Run all tests
npm run test:allMIT




