- 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>
314 lines
9.3 KiB
Markdown
314 lines
9.3 KiB
Markdown
# 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
|