Initialize eng-pad-server with project documentation

- README.md: project overview, quick start, build commands
- CLAUDE.md: AI dev context, source tree, key conventions
- ARCHITECTURE.md: full system spec covering data model, auth
  (password + FIDO2/U2F), gRPC sync API, REST API, SVG/JPG/PDF
  rendering, web UI, configuration, deployment, security
- PROJECT_PLAN.md: 11 phases with discrete checkboxable steps
- PROGRESS.md: decision log and completion tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 19:42:38 -07:00
commit 0cce04b5b8
5 changed files with 672 additions and 0 deletions

313
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,313 @@
# ARCHITECTURE.md — eng-pad-server
## 1. System Overview
eng-pad-server is a read-only sync and viewing service for eng-pad
engineering notebooks. The Android app is the sole writer; the server
receives complete notebook data via gRPC and serves it through a web UI.
```
Android App eng-pad-server
+-----------+ gRPC/TLS +------------------+
| eng-pad | ───────────────> | Sync Service |
| (writer) | username/pass | (gRPC :9443) |
+-----------+ in metadata +------------------+
+──────────────+
│ SQLite DB │
+──────────────+
┌────────────┼────────────┐
▼ ▼ ▼
REST API Web UI Share Links
(:8443) (:8080) (/s/:token)
JSON htmx/SVG No auth
```
## 2. Data Model
### Users
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| username | TEXT UNIQUE | Login identifier |
| password_hash | TEXT | Argon2id hash |
| created_at | INTEGER | Epoch millis |
| updated_at | INTEGER | Epoch millis |
### WebAuthn Credentials
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| user_id | INTEGER FK | References users(id) CASCADE |
| credential_id | BLOB UNIQUE | WebAuthn credential ID |
| public_key | BLOB | COSE public key |
| name | TEXT | User-assigned label |
| sign_count | INTEGER | Signature counter |
| created_at | INTEGER | Epoch millis |
### Notebooks
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| user_id | INTEGER FK | References users(id) CASCADE |
| remote_id | INTEGER | App-side notebook ID |
| title | TEXT | Notebook title |
| page_size | TEXT | "REGULAR" or "LARGE" |
| synced_at | INTEGER | Last sync epoch millis |
UNIQUE(user_id, remote_id)
### Pages
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| notebook_id | INTEGER FK | References notebooks(id) CASCADE |
| remote_id | INTEGER | App-side page ID |
| page_number | INTEGER | 1-based page number |
UNIQUE(notebook_id, remote_id)
### Strokes
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| page_id | INTEGER FK | References pages(id) CASCADE |
| pen_size | REAL | Width in canonical points (300 DPI) |
| color | INTEGER | ARGB packed int |
| style | TEXT | "plain", "dashed", "arrow", "double_arrow" |
| point_data | BLOB | Packed LE floats: [x0,y0,x1,y1,...] |
| stroke_order | INTEGER | Z-order within page |
### Share Links
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| notebook_id | INTEGER FK | References notebooks(id) CASCADE |
| token | TEXT UNIQUE | 32-byte random, URL-safe base64 |
| expires_at | INTEGER | Epoch millis, NULL = never |
| created_at | INTEGER | Epoch millis |
## 3. Authentication
### gRPC (Android App Sync)
- Username and password sent in gRPC metadata on every RPC
- Unary interceptor verifies against Argon2id hash
- TLS required — plaintext rejected
- No tokens, no login RPC
### Web UI (Browser)
- `POST /v1/auth/login` — password → bearer token
- Token in `HttpOnly; Secure; SameSite=Strict` cookie
- 24h TTL, SHA-256 keyed lookup with cache
### FIDO2/U2F (Web UI Only)
- Register keys after password login
- Login with key as password alternative
- Multiple keys per user, user-assigned labels
- `go-webauthn/webauthn` library
### Shareable Links
- Token in URL, no auth required
- 32-byte `crypto/rand`, URL-safe base64
- Optional expiry (default: never)
- Expired → 410 Gone
- Revocable via gRPC API or web UI
## 4. gRPC API
Service: `engpad.v1.EngPadSync`
| RPC | Description | Auth |
|-----|-------------|------|
| SyncNotebook | Push complete notebook (upsert) | user/pass |
| DeleteNotebook | Remove notebook from server | user/pass |
| ListNotebooks | List user's synced notebooks | user/pass |
| CreateShareLink | Generate shareable URL | user/pass |
| RevokeShareLink | Invalidate a share link | user/pass |
| ListShareLinks | List links for a notebook | user/pass |
### Sync Semantics
`SyncNotebook` is a full replacement: all pages and strokes for the
notebook are deleted and re-inserted. The server mirrors exactly what
the tablet has. No incremental sync, no conflict resolution.
Keyed by (user_id, remote_id) where remote_id is the app-side
notebook ID.
## 5. REST API
### Auth Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | /v1/auth/login | None | Password login → token |
| POST | /v1/auth/webauthn/register/begin | Bearer | Start key registration |
| POST | /v1/auth/webauthn/register/finish | Bearer | Complete registration |
| POST | /v1/auth/webauthn/login/begin | None | Start key login |
| POST | /v1/auth/webauthn/login/finish | None | Complete key login |
### Notebook Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /v1/notebooks | Bearer | List notebooks |
| GET | /v1/notebooks/:id | Bearer | Notebook + page list |
| GET | /v1/notebooks/:id/pages/:num/svg | Bearer | Page as SVG |
| GET | /v1/notebooks/:id/pages/:num/jpg | Bearer | Page as JPG (300 DPI) |
| GET | /v1/notebooks/:id/pdf | Bearer | Full notebook PDF |
### Share Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /s/:token | None | Notebook view |
| GET | /s/:token/pages/:num/svg | None | Page SVG |
| GET | /s/:token/pages/:num/jpg | None | Page JPG |
| GET | /s/:token/pdf | None | Notebook PDF |
## 6. Rendering
### SVG
Strokes rendered as SVG `<path>` elements. Coordinates scaled from
300 DPI to 72 DPI (×0.24) for standard SVG/PDF point units.
- `stroke-linecap="round"`, `stroke-linejoin="round"`
- Dashed strokes: `stroke-dasharray="7.2 4.8"`
- Arrow heads: separate `<line>` elements
- No grid — grid is a tablet writing aid only
- `viewBox` matches physical page dimensions in points
### JPG
Server-side rasterization at 300 DPI using Go's `image` package.
White background, strokes rendered with the same coordinate system.
### PDF
Generated with a Go PDF library. Coordinates in 72 DPI (native PDF
points). One page per notebook page.
## 7. Web Interface
Built with Go `html/template` + htmx. Embedded via `//go:embed`.
### Pages
| Route | Template | Description |
|-------|----------|-------------|
| /login | login.html | Login form (password + WebAuthn) |
| /notebooks | notebooks.html | Notebook list |
| /notebooks/:id | notebook.html | Page grid with thumbnails |
| /notebooks/:id/pages/:num | page.html | Full page SVG viewer |
| /s/:token | notebook.html | Shared notebook (no auth) |
| /s/:token/pages/:num | page.html | Shared page viewer |
### Security
- CSRF via signed double-submit cookies
- Session cookie: HttpOnly, Secure, SameSite=Strict
- html/template auto-escaping
## 8. Configuration
```toml
[server]
listen_addr = ":8443"
grpc_addr = ":9443"
tls_cert = "/srv/eng-pad-server/certs/cert.pem"
tls_key = "/srv/eng-pad-server/certs/key.pem"
[web]
listen_addr = ":8080"
base_url = "https://pad.metacircular.net"
[database]
path = "/srv/eng-pad-server/eng-pad-server.db"
[auth]
token_ttl = "24h"
argon2_memory = 65536
argon2_time = 3
argon2_threads = 4
[webauthn]
rp_display_name = "Engineering Pad"
rp_id = "pad.metacircular.net"
rp_origins = ["https://pad.metacircular.net"]
[log]
level = "info"
```
## 9. Deployment
### Container
Multi-stage Docker build:
1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary
2. Runtime: `alpine:latest`, non-root user
### systemd
| Unit | Purpose |
|------|---------|
| eng-pad-server.service | Main service |
| eng-pad-server-backup.service | Oneshot backup |
| eng-pad-server-backup.timer | Daily 02:00 UTC |
Security hardening: NoNewPrivileges, ProtectSystem=strict,
ReadWritePaths=/srv/eng-pad-server.
### Data Directory
```
/srv/eng-pad-server/
├── eng-pad-server.toml
├── eng-pad-server.db
├── certs/
│ ├── cert.pem
│ └── key.pem
└── backups/
```
## 10. Security
- TLS 1.3 minimum, no fallback
- Argon2id for password hashing
- `crypto/rand` for all tokens and nonces
- `crypto/subtle` for constant-time comparisons
- No secrets in logs
- Default deny: unauthenticated requests rejected
- Share links: scoped to single notebook, optional expiry, revocable
- Graceful shutdown: SIGINT/SIGTERM → drain → close DB → exit
## 11. CLI Commands
| Command | Purpose |
|---------|---------|
| server | Start the service |
| init | Create database, first user |
| snapshot | Database backup (VACUUM INTO) |
| status | Health check |
## 12. Future Work
- MCIAS integration for auth delegation
- Per-page share links (URL structure already supports it)
- Notebook version history (store previous syncs)
- WebSocket notifications for real-time sync status
- Thumbnail generation for notebook list