# 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 `` 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 `` 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