7.3 KiB
✏️ InkNote
A cross-platform note-taking application with rich text editing, freehand drawing, PDF annotation, and encrypted sync — inspired by Samsung Notes.
Features
Core
- Rich Text Notes — Bold, italic, underline, headings, bullet/numbered lists, checkboxes
- Freehand Drawing — Pen, highlighter, eraser with adjustable thickness and color palette (20 colors, 7 pen sizes)
- Undo/Redo — Full history stack for drawing operations
- PDF Import & Annotation — Import PDFs, overlay drawing/highlighting, persistent annotations
- Notebooks & Folders — Create notebooks with custom gradient covers, organize notes by folder
- Star/Favorite — Quick access to important notes
- Dark & Light Mode — Full theme support with refined color schemes
Sync & Storage
- Offline-First — Works fully offline, syncs when connection returns
- Conflict-Aware Sync — Vector clock–based conflict detection; notifies user of merge conflicts
- WebSocket Real-Time Push — Changes appear on other devices instantly
- Encrypted at Rest — AES-256-CBC encryption for all note content on the server
- JWT Authentication — Secure user accounts with bcrypt password hashing
Architecture
- Self-Hosted Backend — Node.js + Express + SQLite (or PostgreSQL)
- Docker Ready — docker-compose up and you're running
- Client Sync Engine — Portable JS library for any frontend (React Native, Flutter JS bridge, Electron, web)
Project Structure
inknote/
├── note-app.jsx # Full React UI (artifact / web)
├── docker-compose.yml # Container orchestration
├── backend/
│ ├── server.js # Express API + WebSocket server
│ ├── sync-client.js # Client-side sync engine library
│ ├── package.json # Node dependencies
│ ├── Dockerfile # Server container
│ └── .env.example # Environment variable template
└── README.md # This file
Quick Start
1. Run the UI (Web / Artifact)
The note-app.jsx file is a self-contained React component. It runs directly as a Claude artifact, or you can embed it in any React project:
import NoteApp from './note-app';
function App() {
return <NoteApp />;
}
2. Run the Backend
cd backend
cp .env.example .env # Edit secrets!
npm install
npm start # → http://localhost:4000
3. Docker Deployment
# Generate a secure encryption key
export ENCRYPTION_KEY=$(openssl rand -hex 32)
export JWT_SECRET=$(openssl rand -base64 32)
docker-compose up -d
The server runs on port 4000, the web UI on port 3000.
API Reference
Authentication
| Method | Endpoint | Body | Response |
|---|---|---|---|
| POST | /api/auth/register |
{email, password, displayName} |
{token, user} |
| POST | /api/auth/login |
{email, password} |
{token, user} |
Notebooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/notebooks |
List all notebooks |
| POST | /api/notebooks |
Create notebook |
| PUT | /api/notebooks/:id |
Update notebook |
| DELETE | /api/notebooks/:id |
Soft-delete notebook |
Notes
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/notes |
List all notes (metadata) |
| GET | /api/notes/:id |
Get note with decrypted content |
| POST | /api/notes |
Create note |
| PUT | /api/notes/:id |
Update note (conflict-aware) |
| DELETE | /api/notes/:id |
Soft-delete note |
| PUT | /api/notes/:id/drawing |
Save drawing data (base64) |
| POST | /api/notes/pdf |
Upload PDF (multipart/form) |
Sync
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/sync |
Bidirectional sync with conflict reports |
WebSocket
Connect to ws://localhost:4000/ws, then send:
{"type": "auth", "token": "JWT_TOKEN", "deviceId": "device-123"}
Receive real-time events: note_created, note_updated, note_deleted, notebook_created, etc.
Sync Engine (Client)
import { SyncEngine } from './sync-client';
const sync = new SyncEngine('https://your-server.com', 'device-abc');
// Authenticate
await sync.authenticate('user@example.com', 'password');
// Listen for events
sync.on('status', (status) => console.log('Sync:', status));
sync.on('conflict', (conflict) => {
// Present both versions to user, let them choose
console.log('Conflict:', conflict);
});
sync.on('remote_change', (change) => {
// Update local state with remote changes
console.log('Remote change:', change);
});
// Start syncing (every 30s + WebSocket push)
sync.start();
// CRUD operations auto-queue for sync
await sync.createNote({ type: 'text', title: 'Hello', content: '<p>World</p>' });
await sync.updateNote('note-id', { title: 'Updated' });
await sync.deleteNote('note-id');
await sync.uploadPDF(fileBlob);
await sync.saveDrawing('note-id', canvasDataUrl);
Conflict Resolution
InkNote uses vector clocks to detect conflicts:
- Each device maintains a logical clock counter
- On save, the device increments its counter and includes the full vector clock
- The server compares clocks — if the client clock is strictly newer, the update applies
- If clocks are concurrent (both have changes the other doesn't), a 409 Conflict is returned
- The client receives both versions and can present a merge UI to the user
Extending to Native Platforms
React Native
Use note-app.jsx components with React Native adaptations (replace HTML canvas with react-native-canvas or @shopify/react-native-skia for drawing). The sync engine works as-is.
Flutter
Use the sync engine via a JavaScript bridge, or port the API calls to Dart. For drawing, use Flutter's CustomPainter. For PDF, use flutter_pdfview.
Electron
Wrap the React app directly in Electron for desktop. Add native file system access for PDF import/export.
Security Notes
- All note content is encrypted with AES-256-CBC before database storage
- Passwords are hashed with bcrypt (12 rounds)
- JWT tokens expire after 30 days
- HTTPS should be enforced in production (use a reverse proxy like Nginx or Pangolin)
- The encryption key should be stored securely (e.g., environment variable, secrets manager)
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React (web), adaptable to RN/Flutter |
| Backend | Node.js, Express |
| Database | SQLite (dev), PostgreSQL (prod) |
| Real-time | WebSocket (ws library) |
| Auth | JWT + bcrypt |
| Encryption | AES-256-CBC |
| Container | Docker + docker-compose |