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.
| Language | Go 1.26, standard library first |
| HTTP | net/http with the chi v5 router |
| Database | SQLite (pure Go driver modernc.org/sqlite, no CGO), WAL mode, two separate files |
| Views | html/template, embedded into the binary with embed; plain CSS, no JavaScript framework |
| Config | One .env file, routes.yaml for what to monitor |
| Mail | Gmail SMTP with an app password, standard library net/smtp |
| Deploy | One 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
| Method | Path | What | Access |
| GET | / | Check form | Public |
| POST | /check | Run a check | Public, rate limited |
| GET | /tech | This page | Public |
| POST | /login | Admin sign-in | Public, rate limited |
| GET | /admin | Visitor checks, filters, delete | Admin |
| GET | /admin/routes | Route results, recent pings | Admin |
| POST | /admin/routes/run | Start a check, a summary mail or a ping | Admin, CSRF |
Back to the checker