Tech stack & architecture

David Assistant is a server-rendered Go web app. It started as a workout analyser that read Garmin screenshots, grew into a health and records tracker, and is now becoming a personal dashboard with weather, news and a food log. This page is for developers and recruiters: what it is, how it is built, what was decided on purpose, and what was not verified.

The dashboard of David Assistant: a sidebar with the main pages, a weather widget for Bucharest, this week's workouts and training time, a bar chart of training time per day, a table of workouts by type and the latest daily readings.
The dashboard of a test account: weather, this week's training, training time per day and workouts by type.

How it evolved

  1. Workout analyser. Upload a screenshot from Garmin, Apple Watch, Fitbit or Polar; the server reads it with local OCR, pulls out the type, time, distance, heart rate, training effect and sets, and keeps a training history. Several screens of one workout are merged. The first roadmap planned a vision LLM; the built version uses Tesseract on the server, so health screenshots never leave it.
  2. Daily readings and reports. Daily summary screens (Apple Health, the Garmin Connect dashboard) give steps, sleep, body battery, HRV and more. The Garmin dashboard is a grid of cards that OCR mixes up when read as one page, so the image is cut into its cards and each card is read alone. Weekly and monthly reports compare periods and export CSV.
  3. Medical records and private notes. Lab reports as PDF, image or Word, a parser for lab values with their reference ranges, typed blood values, and notes that are encrypted so that even the admin cannot read them.
  4. Operations. An admin area (users, workouts, uploaded files, logs), request and action logs stored in the database, and a deployment on a VPS behind nginx with HTTPS.
  5. Personal dashboard. The product became David Assistant: a weather widget, news from RSS feeds the user adds, and a food log whose photo check runs in the browser. A sidebar replaced the header navigation.

Stack

LanguageGo 1.26, standard library first. Three direct third-party modules: chi, the SQLite driver and x/crypto
HTTPnet/http with the chi v5 router
DatabaseSQLite through the pure Go driver modernc.org/sqlite (no CGO), WAL mode, one writer connection and a separate reader pool. 19 tables in one schema.sql
Viewshtml/template embedded in the binary with embed; plain CSS with colour tokens; vanilla ES6 JavaScript, no framework and no build step
Authbcrypt passwords, server-side sessions in SQLite (only the hash of the token is stored), HTTP-only cookie, CSRF protection with http.CrossOriginProtection
Reading images and filesTesseract 5 and poppler (pdftotext, pdftoppm) as child processes, two at a time
EncryptionAES-256-GCM and scrypt from x/crypto for private notes
Outside servicesOpen-Meteo (weather), RSS and Atom feeds (news), a CLIP model run in the visitor's browser (food photos)
DeployOne static binary (CGO_ENABLED=0) under pm2, nginx in front, certbot for HTTPS

Architecture

browser ─► nginx (HTTPS) ─► one Go binary (pm2)
  │          routes ─► logging + auth middleware ─► controllers
  │                                                    │
  │                                       services (business rules)
  │                                                    │
  │                  ┌─────────────────────────────────┼───────────────────────────┐
  │                  ▼                                 ▼                           ▼
  │          repositories (SQL only)         child processes: tesseract,    guarded HTTP clients
  │                  │                       pdftotext, pdftoppm           (Open-Meteo, RSS feeds)
  │                  ▼                                                              │
  │       SQLite (WAL) + uploads/<user>/                          answers cached in SQLite
  │
  └─ food photo ─► CLIP model in the browser: the photo never leaves it

The layout is MVC on purpose, not the usual Go style: models holds every struct, constants every constant, and dependencies point downwards only (routes, controllers, services, repositories). Controllers are thin and repositories contain only SQL.

Notable engineering

  • Reading screenshots. Workout screens are converted to black and white (Otsu threshold, dark mode inverted) and parsed by label and plausible range: "first number that makes sense" beats "first number". A daily dashboard is cut into cards by finding the background gutters, each card is read at twice the size, and the number inside a ring (Body Battery, Training Readiness) is read again from the middle of the card. A file is stored once per user by its hash, images are re-encoded (which drops EXIF), and OCR runs through a two-slot limiter.
  • Private notes. A random data key encrypts every note with AES-256-GCM, with the user id as authenticated data so a row cannot be moved to another account. That key is wrapped with a key derived from the password (scrypt) and the unwrapped key lives only in an HTTP-only cookie. The server cannot read the notes and the admin has no query for them. The cost is deliberate: a forgotten password loses the notes.
  • Logs and audit trail. A middleware records every request (route pattern, status, duration, IP) and recovers panics. Routes are marked with an action name, so each login, upload, delete, file view and admin view is also written to an audit table with its outcome. Events go through a bounded buffer to one background writer that batches inserts, so a request is never slowed by logging; rows older than 90 days are purged. The admin viewer has filters, pagination and a details modal.
  • Calling outside APIs. Always from the server, with short timeouts, with the answers cached in SQLite and an older answer used if the service is down. News feeds are addresses typed by users, so the HTTP client refuses private, loopback, link-local and CGNAT addresses at connect time (so a name that resolves to one, or a redirect to one, is stopped too), allows https only, caps redirects and size, and ignores proxy settings.
  • Machine learning in the browser. The food photo is checked with CLIP zero-shot in two steps: first "food or not" with a few broad prompts (a cat, a receipt and a street are refused), then which food from the list. Thresholds were set by running the same pipeline on real photos: food scored 0.75 to 0.99, other images 0.00 to 0.55. The server treats the result as an untrusted hint: it limits and re-encodes it, and a failed check never blocks saving.
  • Rate limits. A sliding window per address or per user, kept in memory, with a 429 page and a Retry-After header. Failed logins (10 per 15 minutes per address), registrations, uploads and document reads (together, per user), added feeds, and a broad limit on all pages. The client address comes only from the proxy's X-Real-IP, and only for a connection from the proxy itself, because headers such as True-Client-IP are written by the client and would let it pick a new identity for every request. A refused request shows in the audit log as rate_limited.
  • SQLite, used properly. WAL, foreign keys and a busy timeout from the connection string, one writer connection, batched writes for logs, partial unique indexes for built-in and user foods, and calories computed from the food table instead of stored.

Design decisions

  • Standard library first. No ORM, no dependency-injection framework, no interfaces until two implementations need them.
  • Privacy by default. Reading another user's data goes through explicit admin methods only, and some data (notes, lab values) has none. Admin access to files is visible in the logs.
  • One schema file, no migration tool. CREATE ... IF NOT EXISTS on every start; a change to an existing table is a manual, backed-up ALTER. Simple, and the cost is visible.
  • Local processing over cloud APIs. OCR on the server and the food model in the browser: no per-request cost and no health data sent to a third party.
  • Soft deletes where users expect an undo. Workouts, readings, medical documents and meals are hidden, not erased.
  • Progressive enhancement. Every page works without JavaScript; scripts add modals, the sidebar, the photo check and extra rows.

Project structure

cmd/server/        wiring only: config, database, layers, routes, listen
internal/
  models/          every struct (domain, forms, view data, API answers)
  constants/       routes, limits, messages, action names
  database/        opens SQLite, schema.sql, foods_seed.csv
  repositories/    SQL only, parameterised, one file per area
  services/        business rules: parsing, OCR, reports, notes crypto, news, weather, food, logs
  controllers/     thin HTTP handlers
  middleware/      logging and audit, auth, admin guard
  routes/          the only place routes are registered
  views/           templates, CSS, JavaScript (embedded)
  testutil/        test database and a fully wired app
docs/              plans and a change log per folder
deploy/            pm2 and nginx examples

Areas of the site

AreaPathsAccess
Workouts/activities, /activities/uploadSigned in
Daily readings/daily, /daily/upload, /daily/newSigned in
Reports/reports, /reports/export.csvSigned in
Medical/medical, /medical/blood, /medical/valuesSigned in
Notes/notesSigned in, encrypted
Food/food, /food/new, /food/foodsSigned in
News/newsSigned in
Admin/admin, /admin/files, /admin/logsAdmin only (404 for others)
Public/auth/login, /auth/register, /health, /techAnyone
  • Screenshot reading was tuned on a few Garmin and Apple screens; another app layout may need new rules.
  • The food photo thresholds come from about 17 test images. They are constants at the top of one file, and need more real photos.
  • Google News feeds are unofficial and can change; a feed that fails is named on the page and the others still show.