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:
313
ARCHITECTURE.md
Normal file
313
ARCHITECTURE.md
Normal 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
|
||||
Reference in New Issue
Block a user