Files
eng-pad-server/ARCHITECTURE.md
Kyle Isom 0cce04b5b8 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>
2026-03-24 19:42:38 -07:00

314 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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