feat: Crumbs by Bretzel — self-hostable notes app MVP

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-05 21:16:45 +01:00
committed by Frederic Cabassut
commit 1becc17724
89 changed files with 11359 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
# API Reference
All API endpoints require authentication (session cookie) except `/api/auth/setup` and `/api/auth/login`.
## Authentication
### POST /api/auth/setup
First-time setup. Creates the single user account.
**Request:**
```json
{ "password": "min8characters" }
```
**Response:** `201 Created`
```json
{ "success": true }
```
Sets `session` cookie.
### POST /api/auth/login
**Request:**
```json
{ "password": "yourpassword" }
```
**Response:** `200 OK`
```json
{ "success": true }
```
Sets `session` cookie.
### POST /api/auth/logout
Clears session.
**Response:** `200 OK`
```json
{ "success": true }
```
## Notes
### GET /api/notes
List notes by filter.
**Query Parameters:**
- `filter` - `all` (default), `archived`, `trashed`
**Response:** `200 OK`
```json
[
{
"id": "uuid",
"title": "My Note",
"content": "**Markdown** content",
"color": "coral",
"pinned": true,
"archived": false,
"trashed": false,
"checklistMode": false,
"sortOrder": 0,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z",
"version": 1,
"tags": ["work", "important"]
}
]
```
### POST /api/notes
Create a new note.
**Request:**
```json
{
"title": "My Note",
"content": "Note content with #tags",
"color": "default",
"pinned": false,
"checklistMode": false
}
```
**Response:** `201 Created` - Returns the created note.
### GET /api/notes/:id
Get a single note.
### PATCH /api/notes/:id
Update a note.
**Request:** (partial update - only include changed fields)
```json
{
"title": "Updated Title",
"color": "coral",
"pinned": true
}
```
**Response:** `200 OK` - Returns the updated note.
### DELETE /api/notes/:id
Permanently delete a note.
## Search
### GET /api/search
Full-text search across notes.
**Query Parameters:**
- `q` - Search query
**Response:** `200 OK` - Array of matching notes (excluding trashed).
## Tags
### GET /api/tags
List all tags.
**Response:** `200 OK`
```json
[
{ "id": 1, "name": "work" },
{ "id": 2, "name": "personal" }
]
```
## Attachments
### GET /api/notes/:id/attachments
List attachments for a note.
### POST /api/notes/:id/attachments
Upload an image attachment.
**Request:** `multipart/form-data` with `file` field.
**Response:** `201 Created` - Returns attachment metadata.
### DELETE /api/notes/:id/attachments?attachmentId=xxx
Delete an attachment.
## Sync
### POST /api/sync
Push local changes to server.
**Request:**
```json
{
"changes": [
{
"noteId": "uuid",
"operation": "create|update|delete",
"timestamp": 1704067200000,
"data": { "title": "...", "content": "..." }
}
]
}
```
### GET /api/sync?since=timestamp
Pull changes since a timestamp (milliseconds).
**Response:** `200 OK` - Array of notes updated since the given timestamp.
+106
View File
@@ -0,0 +1,106 @@
# Architecture
## Overview
Crumbs follows a **local-first** architecture where the client (browser) is the primary data store and the server acts as a sync target and backup.
```
┌─────────────────────────────┐
│ Browser (PWA) │
│ ┌───────────┐ ┌──────────┐ │
│ │ Svelte │ │ IndexedDB│ │
│ │ Stores │◄│ (idb) │ │
│ └─────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ┌─────▼─────────────▼─────┐ │
│ │ Sync Engine │ │
│ │ (LWW CRDT + Queue) │ │
│ └──────────┬──────────────┘ │
└─────────────┼────────────────┘
│ HTTP (JSON)
┌─────────────▼────────────────┐
│ Server (Node.js) │
│ ┌──────────┐ ┌────────────┐ │
│ │ SvelteKit│ │ SQLite │ │
│ │ API │◄│ (Drizzle) │ │
│ └──────────┘ └────────────┘ │
└──────────────────────────────┘
```
## Data Flow
### Read Path
1. User opens app
2. Svelte stores read from IndexedDB (instant)
3. Background sync pulls latest from server
4. If newer data found, IDB is updated, stores react
### Write Path
1. User creates/edits note
2. Change written to IndexedDB immediately
3. Change added to sync queue
4. Background sync pushes queue to server
5. Server applies changes with LWW resolution
### Conflict Resolution (LWW)
For a single-user app with multiple devices:
- Each note has an `updatedAt` timestamp and `version` number
- When merging: newer timestamp wins
- If timestamps match: higher version wins
- If both match: local version is preferred
## Tech Stack Rationale
### SvelteKit
- Smallest bundle size of major frameworks
- Built-in SSR, routing, API routes
- Svelte 5 runes for reactive state
- Native service worker support via @vite-pwa/sveltekit
### SQLite + Drizzle
- Zero-config database (single file)
- WAL mode for concurrent reads
- Drizzle provides type-safe queries without ORM overhead
- Perfect for single-user self-hosted apps
### IndexedDB (idb)
- Browser-native storage (no size limits like localStorage)
- Async API doesn't block main thread
- `idb` library provides Promise-based wrapper
- Survives browser restarts
### Tailwind CSS 4
- Utility-first, tree-shakeable
- Built-in dark mode support (`dark:` variants)
- New v4 uses Vite plugin (faster builds)
### Argon2
- Current best practice for password hashing
- Memory-hard (resists GPU attacks)
- Winner of the Password Hashing Competition
## Database Schema
See `src/lib/server/db/schema.ts` for the complete Drizzle schema.
Key tables:
- `users` - Single user (password hash)
- `sessions` - Auth sessions (30-day TTL)
- `notes` - Core note data with version tracking
- `tags` - Unique tag names
- `note_tags` - Many-to-many note ↔ tag
- `attachments` - Image file metadata
- `sync_log` - Sync operation history
## File Organization
```
src/lib/server/ # Server-only code (DB, auth, attachments)
src/lib/sync/ # Sync engine (shared types, server + client implementations)
src/lib/stores/ # Svelte reactive stores (notes, theme, auth, search)
src/lib/components/ # Svelte UI components
src/lib/utils/ # Pure utility functions (markdown, tags, colors)
src/lib/types/ # TypeScript type definitions
src/routes/api/ # REST API endpoints
src/routes/ # Page routes (SvelteKit file-based routing)
```
+125
View File
@@ -0,0 +1,125 @@
# Deployment Guide
## Docker (Recommended)
### Quick Start
```bash
docker compose up -d
```
Open http://localhost:3000 and set your password.
### Custom Configuration
```yaml
# docker-compose.yml
services:
crumbs:
build: .
ports:
- "8080:3000" # Change external port
volumes:
- /path/to/data:/data # Custom data location
environment:
- ORIGIN=https://notes.example.com # Your domain
restart: unless-stopped
```
### Reverse Proxy (nginx)
```nginx
server {
listen 443 ssl;
server_name notes.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (for future live sync)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
Set `ORIGIN=https://notes.example.com` in your docker-compose.yml.
### Backup
The SQLite database and attachments are stored in `/data`:
```bash
# Backup
docker compose exec crumbs tar czf - /data > crumbs-backup.tar.gz
# Or copy the volume directly
docker cp crumbs:/data ./backup
```
### Update
```bash
docker compose pull
docker compose up -d
```
## Manual Deployment (Node.js)
### Prerequisites
- Node.js 22+
- pnpm
### Steps
```bash
# Clone and install
git clone <repo-url> crumbs
cd crumbs
pnpm install
# Build
pnpm build
# Set environment
export DATABASE_URL=./data/crumbs.db
export ORIGIN=http://localhost:3000
export NODE_ENV=production
# Run
node build
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `DATABASE_URL` | `./data/crumbs.db` | SQLite database path |
| `DATA_DIR` | `./data` | Directory for attachments |
| `ORIGIN` | `http://localhost:3000` | Server origin (required for CSRF) |
| `PORT` | `3000` | HTTP port |
| `NODE_ENV` | `development` | Set to `production` for deployment |
## Health Check
The Docker image includes a health check that pings `/login`:
```bash
docker inspect --format='{{.State.Health.Status}}' crumbs
```
## Security Notes
- Always use HTTPS in production (via reverse proxy)
- Set a strong password (min 8 characters recommended)
- The `ORIGIN` variable must match your actual domain for CSRF protection
- Sessions expire after 30 days
- Passwords are hashed with Argon2
+100
View File
@@ -0,0 +1,100 @@
# Features
## Core Features (MVP)
### Notes CRUD
- Create, read, update, and delete notes
- Notes have a title and content body
- Content supports full Markdown syntax
- Auto-save on editor close
### Markdown Support
- **Bold** (`**text**`)
- *Italic* (`*text*`)
- Headings (`# H1` through `###### H6`)
- Unordered lists (`- item`)
- Ordered lists (`1. item`)
- Code blocks (triple backtick)
- Inline code (single backtick)
- Tables
- Links (auto-linkified URLs)
- Line breaks
### Checklists / Task Lists
- Toggle between regular note and checklist mode
- Add, remove, reorder checklist items
- Check/uncheck items with checkboxes
- Checked items get strikethrough styling
- Enter key adds new item, Backspace removes empty items
### Image Attachments
- Upload images via file picker or drag-and-drop
- Supports JPEG, PNG, GIF, WebP, SVG
- Max file size: 10MB
- Thumbnail previews in note cards
- Remove attachments from editor
### Tag Organization
- Inline `#hashtag` extraction from note content
- Tags automatically parsed from title and content
- Filter notes by tag (sidebar + tag chips)
- Tags are case-insensitive and deduplicated
- Tags inside code blocks and URLs are ignored
### Color-Coded Notes
12 colors matching Google Keep's palette:
- Default, Coral, Peach, Sand, Mint, Sage
- Fog, Storm, Dusk, Blossom, Clay, Chalk
- Colors adapt to light/dark mode
### Pin Notes
- Pin important notes to always appear at top
- Pinned section separated from unpinned notes
- Toggle pin from note card hover actions
### Archive
- Archive notes to declutter main view
- Archived notes accessible from sidebar
- Unarchive to restore to main view
### Trash
- Soft delete moves notes to trash
- Trash view accessible from sidebar
- Restore notes from trash
- Permanently delete from trash
### Full-Text Search
- Search across note titles, content, and tags
- Real-time search results as you type
- Clear search to restore original view
- Case-insensitive matching
### Dark Mode
- System preference detection (auto)
- Manual toggle override
- Preference persisted in localStorage
- All components adapt to theme
- Note colors adjust for dark backgrounds
### Single-User Auth
- First-run setup: create password (min 8 chars)
- Argon2 password hashing (industry standard)
- Session-based auth with 30-day expiry
- All routes protected except /login and /setup
- Logout clears session
### PWA / Offline-First
- Installable as standalone app
- Web app manifest with icons
- Service worker for asset caching
- IndexedDB for local data storage
- Background sync every 30 seconds
- LWW conflict resolution for multi-device use
- Sync status indicator (synced/syncing/offline/error)
### Docker Deployment
- Multi-stage Dockerfile (build + slim runtime)
- docker-compose.yml with persistent volume
- Health check endpoint
- Runs as non-root user
- Configurable via environment variables