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

9.3 KiB
Raw Blame History

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
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
  • 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

[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