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.
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
9. Deployment
Container
Multi-stage Docker build:
- Builder:
golang:1.25-alpine, CGO_ENABLED=0, stripped binary
- 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
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