forked from mirror/crumbs-original
feat: Crumbs by Bretzel — self-hostable notes app MVP
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+166
@@ -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.
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user