Files
eng-pad-server/ARCHITECTURE.md
Kyle Isom 691301dade Update docs for Docker-on-deimos deployment, add grpc_plain_addr option
- ARCHITECTURE.md: document nginx + direct gRPC topology, add
  grpc_plain_addr config, update cert filenames to Let's Encrypt
  convention, add passwd to CLI table
- RUNBOOK.md: replace systemctl/journalctl with docker commands,
  fix cert path references, improve sync troubleshooting steps
- Example config: update cert paths, document grpc_plain_addr option
- grpcserver: add optional plaintext gRPC listener for reverse proxy
- config: add GRPCPlainAddr field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:58:01 -07:00

10 KiB
Raw Permalink 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"             # REST API (HTTPS)
grpc_addr      = ":9443"             # gRPC (TLS, exposed directly)
grpc_plain_addr = ""                 # Optional plaintext gRPC for reverse proxy
tls_cert       = "/srv/eng-pad-server/certs/fullchain.pem"
tls_key        = "/srv/eng-pad-server/certs/privkey.pem"

[web]
listen_addr = ":8080"                # Web UI (plain HTTP behind nginx)
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

Production (deimos.wntrmute.net)

Docker container behind nginx on deimos:

  • Web UI: https://pad.metacircular.net — nginx (port 443) → container:8080
  • gRPC sync: pad.metacircular.net:9443 — direct TLS, exposed via ufw
  • REST API: container:8443 — not exposed externally
  • TLS: Let's Encrypt cert for pad.metacircular.net, shared by nginx and the container (copied to /srv/eng-pad-server/certs/)
Internet
  │
  ├── :443 → nginx (TLS termination) → container:8080 (Web UI, plain HTTP)
  └── :9443 → container:9443 (gRPC, direct TLS)

Container

Multi-stage Docker build:

  1. Builder: golang:1.25-alpine, CGO_ENABLED=0, stripped binary
  2. Runtime: alpine:3.21, non-root user (engpad, UID 1000)

systemd (alternative)

systemd units are provided for non-Docker deployments:

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/
│   ├── fullchain.pem          # Let's Encrypt cert chain
│   └── privkey.pem            # Let's Encrypt private key
└── 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
passwd Reset a user's password
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