A local-first, OneNote-style notebook app. All notes are stored as JSON files on your own filesystem — no account, no cloud, no telemetry.
- Notebooks with custom colors — one JSON file per notebook, easy to back up or version-control
- Sections with named creation and a full color picker — color-coded tabs like OneNote
- Pages and sub-pages (one level deep) per section
- Drag-and-drop reordering of pages and sections
- Full-text search across all pages in a notebook (Ctrl+F)
Built on TipTap (ProseMirror):
Formatting
- Bold, Italic, Underline, Strikethrough
- Superscript, Subscript
- Text color and multi-color highlight
- Font family (Sans-serif / Serif / Monospace)
- Font size
- Text alignment (left / center / right)
- Indentation via Tab / Shift+Tab (paragraphs, headings, and nested lists)
Structure
- Headings H1–H4
- Bullet lists, ordered lists, and task lists (checkboxes) — with full nesting support
- Blockquotes
- Code blocks with syntax highlighting (100+ languages via lowlight)
- Inline code
- Horizontal rules
- Callout / info boxes (info, warning, success, error styles)
Tables
- Insert tables with a header row
- Add / remove rows and columns
- Merge and split cells
- Toggle header row
- Mouse-drag column resize
- Manual column width input (percentage)
- Copy column widths from one table and paste to another (proportionally rescaled if column counts differ)
- Vertical cell alignment (top / middle / bottom) per cell
Images
- Upload via toolbar or drag-and-drop
- Paste images directly from the clipboard (screenshots, Finder, etc.)
- Paste rich content containing images — each image is uploaded individually, text is preserved
- Inline resize handle (drag right edge)
- Inline alignment controls (left / center / right / full-width)
YouTube
- Paste any YouTube URL (youtube.com or youtu.be) → automatically replaced with the video thumbnail and a play button; clicking it opens the video in a new tab
Links & Navigation
- Hyperlinks with inline editing
- Internal page links — type
[[to open an autocomplete picker of all pages in any section; renders as a clickable chip that navigates to the target page
- Collapsible outline panel listing all headings on the current page
- Click any entry to scroll the editor to that heading
- Headings inside tables are excluded
- Auto-save — debounced 1 second after the last keystroke
- Manual save — Ctrl+S / Cmd+S for an immediate flush
- Save indicator in the top bar (Saving… → Saved ✓)
- Print — Ctrl+P or toolbar button; hides all UI chrome, resets layout to a clean page
- Export — download a full notebook as a ZIP of HTML files (one per page) plus all images
- Page width — drag the right-edge handle or pick a preset (Narrow / Medium / Wide) from the toolbar
- Light and dark mode; follows system preference by default, overridable in Settings
Docker handles all dependencies — no Node.js installation required.
docker-compose up appFirst run installs npm packages inside the container (~1–2 min). Once you see Starting dev servers...:
- App → http://localhost:5173
- API → http://localhost:3001
# Stop (keeps data and installed packages)
docker-compose stop
# Stop and remove containers (keeps data, discards package cache)
docker-compose down
# Stop and remove everything including named volumes
docker-compose down -v
# Rebuild after changing dependencies
docker-compose up --build app# Install all workspace dependencies
npm install
# Start both the API server and the Vite dev server in parallel
npm run dev- App → http://localhost:5173
- API → http://localhost:3001
By default, notebooks and images are stored in ~/Documents/NoteVault/. Change this in Settings.
npm run dev:server # API only → :3001
npm run dev:client # UI only → :5173npm run build # compiles server TypeScript + bundles React client
npm run lint # lint all workspacesThe Dockerfile has a multi-stage production build that compiles the TypeScript server and bundles the React client into a single Node.js image (~120 MB). The server serves both the API and the static frontend from one port.
docker build --target production -t notevault .docker run -d \
--name notevault \
-p 3001:3001 \
-v /your/data/path:/app/storage \
notevaultReplace /your/data/path with the absolute path on the host where you want notebooks and images to live. The app is then available at http://localhost:3001.
| Variable | Default | Description |
|---|---|---|
PORT |
3001 |
Port the server listens on |
STORAGE_PATH |
~/Documents/NoteVault |
Absolute path to the storage directory |
CLIENT_ORIGIN |
http://localhost:5173 |
Allowed CORS origin (dev only; ignored in production) |
NODE_ENV |
— | Set to production to enable static file serving from client/dist |
The production container exposes a single port. Point your proxy at it and increase the client body size for image uploads:
nginx
location / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 20m;
}Caddy
your.domain.com {
reverse_proxy localhost:3001
}
Security note: NoteVault is a personal, single-user tool. If you expose it on the internet, put it behind a firewall, VPN, or HTTP basic auth at the proxy level.
Packages NoteVault as a native .app / .dmg for macOS. The Express server runs inside the Electron process — no separate terminal or browser needed. Data is stored in ~/Library/Application Support/NoteVault/storage/.
Node.js 20+ installed locally.
# Install dependencies (once)
npm install
# Build client + server + Electron main process, then package
npm run electron:packThe output lands in dist-app/ — a universal .dmg for both Apple Silicon and Intel (arm64 + x64).
npm run electron:devBuilds everything and opens an Electron window against the local server. Useful for testing before packaging.
This is an npm workspaces monorepo with two packages: client/ and server/.
notevault/
├── client/ # React + TipTap frontend (Vite)
│ └── src/
│ ├── components/
│ │ ├── editor/ # TipTap editor, toolbar, table menu, outline panel
│ │ ├── layout/ # AppShell, three-panel layout, TopBar
│ │ ├── modals/ # Notebook / section / confirm dialogs, color picker
│ │ └── sidebar/ # Notebook tree, section list, page list
│ ├── store/ # Zustand: notebookStore, uiStore
│ ├── hooks/ # useAutoSave, usePage, …
│ └── api/ # Axios wrapper (api/client.ts)
│
├── server/ # Express REST API
│ └── src/
│ ├── routes/ # notebooks, sections, pages, images, settings, auth
│ └── services/
│ └── fileService.ts # All reads/writes; no ORM, no database
│
├── storage/ # Data directory (mounted as a volume in Docker)
│ ├── notebooks/ # One <uuid>.json per notebook + index.json
│ └── images/ # Uploaded images
│
├── electron/ # Electron wrapper for the Mac app
├── Dockerfile
└── docker-compose.yml
Each notebook is one self-contained JSON file. The hierarchy is:
Notebook → SectionGroup (optional) → Section → Page → SubPage (one level)
Page content is a TipTap JSON document (ProseMirror AST). This means individual pages are human-readable and portable — any tool that understands ProseMirror JSON can read them.
http://localhost:3001/api
| Resource | Endpoints |
|---|---|
| Notebooks | GET /notebooks · POST /notebooks · `GET |
| Sections | POST /notebooks/:id/sections · `PUT |
| Pages | `GET |
| Images | POST /images/upload · GET /images/:filename |
| Settings | `GET |
| Layer | Technology |
|---|---|
| Frontend framework | React 18 + TypeScript |
| Editor | TipTap v2 (ProseMirror) |
| Styling | Tailwind CSS v3 + custom CSS variables |
| State | Zustand (with persist middleware) |
| Backend | Express + TypeScript |
| Persistence | Flat JSON files — no database |
| Build | Vite (client) · tsc (server) |
| Icons | Lucide React |
| Image upload | Multer |
| Syntax highlighting | lowlight (highlight.js grammar) |
| Desktop app | Electron |
| Container | Docker / Docker Compose |