Tech stack & architecture

goChecker is a small Go service with two jobs: a public "is that site up?" checker, and a private monitor that logs in to another application as test users and reports which pages answer. This page is for developers and recruiters.

LanguageGo 1.26, standard library first
HTTPnet/http with the chi v5 router
DatabaseSQLite (pure Go driver modernc.org/sqlite, no CGO), WAL mode, two separate files
Viewshtml/template, embedded into the binary with embed; plain CSS, no JavaScript framework
ConfigOne .env file, routes.yaml for what to monitor
MailGmail SMTP with an app password, standard library net/smtp
DeployOne static binary, cron for the schedules, nginx in front

Architecture overview

visitor ─► nginx ─► serve (public + admin)
                      │   chi router → middlewares → controllers → models/services
                      │                         │
                      │                    gochecker.db   (public checks)
                      ▼
                 monitor.db  ◄── probe / ping (separate process)
                                  │  logs in as test users, GET only
                                  ▼
                           monitored app ──► Gmail alert on failure

cron:  */5 min  gochecker ping
       every 3h gochecker probe

The code follows an MVC-style layout: routes, middlewares, controllers, services, models, views.

Public checker

  • Input normalisation: a visitor-supplied address is parsed and rejected early if it is not a plain http(s) URL.
  • SSRF guard: the dialer refuses private, loopback, link-local (cloud metadata), CGNAT, documentation and reserved ranges for IPv4 and IPv6, including IPv4-mapped IPv6 forms. The check runs on the address actually dialled, so DNS tricks do not bypass it.
  • Fetcher limits: 5 second timeout, at most 3 redirects, a HEAD request first, falling back to GET.
  • Verdict: up, up (restricted) or down, from status code and failure kind.
  • Rate limiting: a sliding window per client IP plus a daily cap for all clients. Limits are configuration values.

Route monitor

  • What to check lives in routes.yaml: sites, test accounts (as environment variable names, never values), fixture ids, and routes with expected status, text or redirect target. Routes with a missing fixture are reported as skipped, not failed.
  • Authenticated probe: reads the login form like a browser (hidden fields included), keeps a cookie jar per role, and issues GET requests only.
  • Labels: ok, slow, error or skipped; a failed route is confirmed once more before it counts; after three connection failures in a row the rest of a site is skipped instead of waiting for every timeout.
  • Request signing: an optional HMAC header over method, URI and timestamp, sent only to the site's own host.
  • One run at a time: a file lock shared by cron and the admin button.
  • Ping: a small 5 minute check of marked routes, stored as its own run kind in the same tables.
  • Alerts: one consolidated mail per run listing failed routes; the ping mails only when the state changes (down, then recovered).

Security

  • Process separation: the public server loads only its own keys from .env; test-account passwords and the mail password are read by the separate probe process only. The admin button starts that process as a child.
  • Secrets never print: a Secret type redacts itself in logs, formatting and JSON; login failures log field names and status codes, never values.
  • Fixture ids stay out of storage: results keep the path template (/profile/{client_uid}), not the real id.
  • Admin: a single admin, bcrypt password hash, per-session CSRF token on every state-changing form, login rate limit.
  • Headers: strict Content-Security-Policy, X-Frame-Options: DENY, Referrer-Policy: no-referrer, request body size limit.
  • Client IP: X-Real-IP is trusted only from configured proxy addresses.

Design decisions

  • Standard library first: small dependency list; the router and the SQLite driver are the main third-party pieces.
  • SQLite instead of a server database: one writer, low volume, nothing to run beside the binary. The monitor has its own file so it can be deleted or moved without touching public data.
  • Child process for admin-started checks: keeps credentials out of the web process and lets cron and the button share one code path.
  • Configuration over code: adding a monitored route is a line in routes.yaml, not a code change.
  • Build the feature, name what was not verified: the project does not add tests by default; the notes in docs/ state what was and was not checked.

Project structure

cmd/gochecker/   serve, probe, ping, hash-password
internal/
  routes/        chi router
  middlewares/   real IP, request log, security headers, body limit
  controllers/   handlers (home, check, login, admin, routes page)
  services/      normalize, SSRF guard, fetcher, verdict
  models/        SQLite access and schemas
  views/         templates and static assets (embedded)
  ratelimit/     sliding window and daily cap
  adminauth/     admin login and sessions
  probe/         routes.yaml loader, runner, launcher, run lock
  alert/         Gmail mail
  config/        .env loader and settings
  retention/     old-record cleanup
routes.yaml      what the monitor checks
deploy/          crontab example

Endpoints

MethodPathWhatAccess
GET/Check formPublic
POST/checkRun a checkPublic, rate limited
GET/techThis pagePublic
POST/loginAdmin sign-inPublic, rate limited
GET/adminVisitor checks, filters, deleteAdmin
GET/admin/routesRoute results, recent pingsAdmin
POST/admin/routes/runStart a check, a summary mail or a pingAdmin, CSRF

Back to the checker