Engineering

How the Go backends are built

Component diagrams, numbered data flows, decisions and trade-offs. All taken from the real code of thirteen demo projects.

01 · job-queue

Conveyor: job queue

A background job queue on SQLite: priorities, retries with backoff, cron, idempotency keys. The point is the guarantees: at-least-once, lease and fencing.

  • Go
  • SQLite
  • REST API
  • SSE
  • Prometheus
  • Docker
  • 25 tests
  • 19 routes
  • 2,161 lines of Go
Component diagram: Conveyor: job queueDiagram of "Conveyor: job queue": 7 components and 7 links; link numbers match the flow list below.API clientPOST /api/jobsSchedulercron schedulesSQLite (WAL)jobs · runs · schedulesClaimUPDATE … RETURNINGReaperexpired leasesWorker poollease heartbeatDashboard and /metricsSSE · Prometheus1234567
Client / entryServiceStorage

Data flows

  1. 1A job is enqueued over REST with a Bearer token; enqueueing again with the same idempotency key returns the existing job.
  2. 2Each cron run is enqueued with the key cron:<id>:<time> and next_run is moved by a conditional UPDATE, so several processes cannot create a duplicate.
  3. 3The highest-priority ready job is picked by priority DESC, run_at, id.
  4. 4A single statement sets the status, attempt counter, lease deadline and a unique lease token, and hands the job to a worker.
  5. 5The worker extends the lease with a heartbeat every third of the term and finishes the job (Complete/Fail) presenting the token: a zombie worker gets ErrLeaseLost.
  6. 6The reaper returns jobs with an expired lease to the queue with backoff, or to dead when attempts are exhausted.
  7. 7A state snapshot goes to the dashboard over SSE once a second; counters and a histogram are exposed on /metrics.

Decisions

Atomic claim with one UPDATE … RETURNING

Picking the job and granting the lease happen in a single statement, so two workers, even from different processes on the same database file, cannot get the same job.

Lease, heartbeat and fencing token

A crashed worker does not lose the job: the lease expires, and Complete/Fail/Extend check the token, so a zombie that wakes up cannot corrupt the result.

At-least-once, not exactly-once

A handler may be called again, so handlers must be idempotent. The queue does not promise what it cannot guarantee.

Backoff with jitter

The delay is BACKOFF_BASE · 2^(n-1) with a cap, multiplied by a random factor of 0.5–1; a Permanent error or an unknown type goes straight to dead.

Trade-offs

  • A single SQLite file: scales only on one machine (several processes on one volume are fine, over the network they are not).
  • Metric counters live in memory and reset on restart; there are no job chains, per-queue rate limits or roles (one token).

Tests

Concurrent claiming (400 jobs, two "processes" on one file, 12 workers: no double execution), lease expiry and fencing, racing idempotency keys, cron from two processes, graceful shutdown.

Source: README of the job-queue project (in Russian), sections "Архитектура" (Architecture), "Гарантии доставки" (Delivery guarantees), "Ограничения" (Limitations)

02 · uptime-monitor

Pulse: uptime monitor

An uptime monitor: HTTP and TCP checks, uptime, p50/p95, incidents, a live dashboard and a public status page.

  • Go
  • SQLite
  • SSE
  • REST API
  • Docker
  • 18 tests
  • 15 routes
  • 1,445 lines of Go
Component diagram: Pulse: uptime monitorDiagram of "Pulse: uptime monitor": 8 components and 9 links; link numbers match the flow list below.Managerone goroutine per checkTargetsHTTP / TCPRuncheck + timingHub (pub/sub)non-blockingSQLite (WAL)checks · results · incidentsstatsp50/p95 · uptimeDetectorincidentsREST + SSEdashboard · /status123456789
Client / entryServiceStorage

Data flows

  1. 1The manager keeps one goroutine per check; the next run is scheduled after the previous one finishes, so checks of one address never overlap.
  2. 2Run performs an HTTP GET (up to 5 redirects, expected status or any < 400) or a TCP connect with a timeout.
  3. 3The result is written to SQLite.
  4. 4After the write, the result is published to the hub.
  5. 5The detector gets the result: fail_threshold failures in a row open an incident, the first success closes it.
  6. 6A state change is stored as an incident; an open incident survives a restart.
  7. 7Uptime for 24 h, 7 and 30 days and p50/p95 are computed by queries over the raw history (pure functions in internal/stats).
  8. 8REST serves the numbers to the dashboard and to the public status page (public checks only, without addresses or error texts).
  9. 9The hub fans events out to SSE subscribers without blocking: a slow client loses events instead of slowing the checks down.

Decisions

Percentiles by the nearest-rank method

The p-th percentile is the element of the sorted slice at rank ceil(p/100·N). A simple pure function without interpolation, easy to test.

Never overlap checks

The next run is scheduled after the previous one completes, so a slow target does not pile up parallel checks on itself.

A slow client does not slow monitoring

Hub delivery is non-blocking; after reconnecting, the dashboard re-reads the state.

SSE over fetch, not EventSource

This way the token travels in the Authorization header instead of the URL.

Trade-offs

  • Uptime is computed from the number of checks, not durations, so figures for checks with different intervals are not equally precise.
  • Aggregates run over the raw history: instant for dozens of checks, thousands would need pre-aggregation. One node, one token, no notifications.

Tests

Scheduler, percentiles and uptime, incident lifecycle (including after a restart), HTTP/TCP checks with a timeout, the API, SSE.

Source: README of the uptime-monitor project (in Russian), sections "Возможности" (Features), "Архитектура" (Architecture), "Ограничения" (Limitations)

03 · realtime-rooms

Rooms: realtime chat and board

WebSocket rooms: chat with history, presence, a shared board. The RFC 6455 protocol is written on the stdlib to show what happens under new WebSocket(…).

  • Go
  • WebSocket
  • SQLite
  • Canvas
  • Docker
  • 48 tests
  • 7 routes
  • 1,822 lines of Go
Component diagram: Rooms: realtime chat and boardDiagram of "Rooms: realtime chat and board": 7 components and 7 links; link numbers match the flow list below.BrowsersWebSocket clientsinternal/wshandshake · frames · limitsRoom hubmutex per roomClient queue256 messageswrite pump10 s write deadlineSQLite (WAL)chat historyBoard in memorystrokes1234567
Client / entryServiceStorage

Data flows

  1. 1The browser opens a WebSocket: GET with Upgrade, version 13, a key and an Origin check; the answer is 101 with Sec-WebSocket-Accept.
  2. 2Frames are parsed in internal/ws: masking, fragmentation, size limit, UTF-8, ping/pong; the JSON message goes to the hub (read pump).
  3. 3Broadcasting puts the message into each client's bounded queue; on overflow a slow client is disconnected with code 1008 and the others do not wait.
  4. 4The write pump takes messages from the queue and writes frames with a 10 s deadline.
  5. 5Frames reach the browsers; ephemeral messages (cursors, typing) are skipped when the queue is 3/4 full.
  6. 6A message is broadcast only after a successful SQLite write; on failure the sender gets error: store.
  7. 7Board strokes are kept in room memory and given to participants who join later.

Decisions

Own WebSocket layer

About 530 lines in internal/ws: handshake, frames, masking, fragmentation, ping/pong and close codes. The goal is to understand the protocol; production usually calls for a proven library.

Backpressure instead of waiting

A bounded queue per client and disconnecting the slow one: a single slow tab does not hold the room back.

Same order for everyone

Saving and broadcasting happen under one room mutex; the history in welcome is read under the same mutex, so there is neither overlap nor a gap with the live stream.

Per-connection limits

A token bucket for chat (2 messages/s, burst 5) and an overall limit (200/s, burst 400); flooding closes the connection with code 1008.

Trade-offs

  • A single process: the hub lives in memory, with no horizontal scaling. The board does not survive a server restart and any participant can clear it.
  • No permessage-deflate, TLS or authentication; the Autobahn Test Suite was not run. Nicknames are anonymous and unprotected.

Tests

The handshake example from the RFC, 12 kinds of protocol violations with close-code checks, fragmentation with a ping in the middle, slow-client disconnects, and a load test: 360 connections in 6 rooms.

Source: README of the realtime-rooms project (in Russian), sections "Почему свой слой WebSocket" (Why a custom WebSocket layer), "Возможности" (Features), "Гарантии" (Guarantees), "Ограничения" (Limitations)

04 · booking-slots

Appointment booking

Online booking: free slots from service duration and working hours, double-booking protection, an admin panel.

  • Go
  • SQLite
  • Docker
  • 12 tests
  • 18 routes
  • 891 lines of Go
Component diagram: Appointment bookingDiagram of "Appointment booking": 6 components and 6 links; link numbers match the flow list below.Clientservice · day · slotAdmin /adminBearer tokenHTTP APIslots · bookings · adminSlot calculationhours minus takenBookingone transactionSQLiteUNIQUE (date, start_min) WHERE active123456
Client / entryServiceStorage

Data flows

  1. 1The client requests 14 days ahead and the slots of a day, then sends POST /api/bookings.
  2. 2The owner manages bookings, services, working hours and closed dates.
  3. 3Slots are computed from the service duration, the weekday hours and the taken intervals, on a 30-minute grid.
  4. 4Book checks that the slot is free and inserts the booking in one transaction.
  5. 5The calculation reads working hours, closed dates and active bookings.
  6. 6The insert is guarded by a partial UNIQUE index; on conflict the client gets 409.

Decisions

Two lines of defence against double booking

The availability check and the insert run in one transaction, and a partial UNIQUE index on (date, start_min) for active bookings catches whatever the check missed. A cancelled booking frees the time.

Time zone as configuration

The zone comes from TZ_NAME and the tzdata database is embedded in the binary, so the schedule does not depend on the machine's settings.

Trade-offs

  • One specialist/resource: all services share one schedule.
  • Closing a date does not cancel existing bookings; the client cannot cancel on their own, there are no notifications; the admin panel uses a shared token.

Tests

Slot calculation and period bounds, overlaps, concurrent booking: 30 goroutines for one time slot, exactly one succeeds.

Source: README of the booking-slots project (in Russian), sections "Возможности" (Features), "Тесты" (Tests), "Ограничения" (Limitations), and the code in internal/booking

06 · kanban-tasks

Amber: kanban board

A multi-user kanban board: registration, boards, columns, drag-and-drop cards.

  • Go
  • SQLite
  • Docker
  • 7 tests
  • 18 routes
  • 871 lines of Go
Component diagram: Amber: kanban boardDiagram of "Amber: kanban board": 5 components and 5 links; link numbers match the flow list below.Browservanilla JS · drag and dropHTTP API (JSON)Content-Type · same-originAuthenticationPBKDF2 · sessionsBoards and cardsper-user dataSQLiteno CGO12345
Client / entryServiceStorage

Data flows

  1. 1The browser talks to the JSON API; mutating requests are checked for Content-Type and same-origin.
  2. 2Sign-up and login by email and password: PBKDF2-HMAC-SHA256, a session in an HttpOnly cookie with SameSite=Lax.
  3. 3Boards, columns and cards are available only to their owner: user data is isolated.
  4. 4Users and sessions are stored in SQLite.
  5. 5Boards, columns, cards and their order are stored in SQLite.

Decisions

Protection without external libraries

PBKDF2 from the standard library, an HttpOnly session, and Content-Type plus same-origin checks for mutating requests.

Trade-offs

  • A learning example: no password reset, email verification or login rate limiting.
  • Do not expose it to the internet without HTTPS and a reverse proxy.

Tests

API tests (go vet and go test in CI).

Source: README of the kanban-tasks project (in Russian), sections "Возможности" (Features), "Ограничения" (Limitations)

07 · api-gateway

Sluice: API gateway

An API gateway on the pure stdlib: routes from a JSON config with hot reload, balancing, health checks, a per-node circuit breaker, retries, rate limiting. The point is immutable state and protection against open-proxy and SSRF.

  • Go
  • stdlib only
  • Reverse proxy
  • Circuit breaker
  • Token bucket
  • SSE
  • Prometheus
  • slog
  • Vanilla JS
  • embed
  • Docker
  • GitHub Actions
  • 48 tests
  • 11 routes
  • 2,334 lines of Go
Component diagram: Sluice: API gatewayDiagram of "Sluice: API gateway": 10 components and 9 links; link numbers match the flow list below.ClientHTTP on :8087OperatorSIGHUP · PUT /api/configGateway entryrequest-id · host + prefixAuth and rate limitkey sha256 · token bucketStateatomic.PointerBalancerround-robin · least-connHealth checksGET /health per nodeCircuit breakerclosed · open · half-openDashboard and /metricsSSE · PrometheusUpstreamsSSRF guard · retries123456789
Client / entryServiceStorage

Data flows

  1. 1A request arrives on :8087, gets a request-id and the route is looked up by host and longest prefix; no match gives 404, the gateway is not an open proxy.
  2. 2The operator changes the config via SIGHUP (Unix only) or PUT/POST in the admin on :8088; the config is validated as a whole, a bad one is rejected and the old one keeps running.
  3. 3The new state (routes, upstreams, keys) is swapped with a single atomic.Pointer: the listening socket is untouched and an in-flight request finishes on the old version.
  4. 4The API key is hashed with sha256 and checked against the config; the token-bucket rate limit is kept per IP (IPv6 by /64) or per key, and an excess request never reaches the upstream.
  5. 5The balancer picks a node by round-robin or least-conn, among healthy nodes only.
  6. 6A separate health loop per node polls GET /health and marks the node healthy or not by thresholds.
  7. 7Each node has its own breaker (closed, open, half-open); a connection failure, a timeout and any 5xx except 501 count as errors, a client aborting the request does not.
  8. 8The connection goes through a guarded Dialer that checks the IP at connect time (SSRF, DNS rebinding); on 502/503/504 or a network failure GET, HEAD, OPTIONS, PUT and DELETE are retried on another node.
  9. 9The statistics window and counters go to the dashboard over SSE and to /metrics in Prometheus format; labels are bounded by the config, paths and keys never become labels.

Decisions

Immutable state behind an atomic.Pointer

Routes, upstreams and keys are built into a new state and swapped with one pointer. Upstreams whose description did not change are reused as is, so health, breaker and counters are not reset on reload.

Not an open proxy, SSRF protection

The destination comes only from the config; Host, absolute-URI, CONNECT/TRACE and paths with ".." cannot escape the routes. The IP a name resolves to is checked again at connect time, and allowing private networks is set by the environment, not the config.

A breaker per node

The closed, open and half-open states are kept per node: one sick node drops out of rotation while the others keep taking traffic.

Retries for idempotent requests only

POST is not repeated so an operation is never run twice; the body is buffered up to 1 MiB and larger requests go without retries.

Trade-offs

  • Rate limit and breaker state live in the memory of one process: with several replicas the limits are not shared, and behind a load balancer all clients look like one IP because the gateway does not trust X-Forwarded-For.
  • No TLS on the inbound side, WebSocket and Upgrade are not supported (501); percentiles come from a histogram with up to 25% error; the admin has a single token; SIGHUP is Unix only.

Tests

Balancers, breaker states on a fake clock, token bucket, SSRF cases, retries (GET, PUT with a body; POST is not retried), hot reload with an in-flight request, open-proxy protection, a load test (4000 requests, 40 clients); go test -race in CI.

Source: README of the api-gateway project (in Russian), sections "Архитектура" (Architecture), "Гарантии" (Guarantees), "Что считается ошибкой" (What counts as an error), "Ограничения" (Limitations)

09 · raft-kv

Raft KV: a key-value store on Raft

A teaching implementation of Raft on the pure stdlib: three nodes, elections, replication, quorum commit, a WAL with fsync, snapshots, read-index. The point is a core with no time and no network, which the same code runs both in the runtime and in a deterministic simulator.

  • Go
  • Raft
  • Distributed systems
  • Consensus
  • stdlib only
  • HTTP/JSON
  • SSE
  • WAL + fsync
  • Snapshots
  • Linearizability
  • Deterministic simulation testing
  • Vanilla JS
  • Docker
  • GitHub Actions
  • 27 tests
  • 13 routes
  • 2,646 lines of Go
Component diagram: Raft KV: a key-value store on RaftDiagram of "Raft KV: a key-value store on Raft": 8 components and 8 links; link numbers match the flow list below.ClientPUT · GET · DELETEDashboard :8098SSE · failures (demo)Node runtimeHTTP/JSON · 25 ms tick · 307raft.NodeTick() · Step(msg)The two other nodesvote · append · snapshotStorageWAL + fsync · state.jsonKV state machineput · del · snapshotSimulatorvirtual time · seed12345678
Client / entryServiceStorage

Data flows

  1. 1A client sends PUT, GET or DELETE to any of the nodes 8095–8097; a non-leader answers 307 with a Location pointing at the leader (and an X-Raft-Leader header), and if the leader is unknown, 503 with Retry-After.
  2. 2Every 200 ms the dashboard gets role, term, commit/applied and the log tail over SSE; the "stop node", "cut the network" and "kill the leader" buttons exist only with SEED_DEMO=1 and go over real HTTP.
  3. 3The runtime ticks the core every 25 ms (Tick) and hands it incoming messages (Step); outgoing messages are returned to the caller, raft.Node itself knows nothing about time or the network.
  4. 4Elections (vote), replication (append) and snapshot travel as HTTP/JSON messages, and replies are separate messages, so the network may drop and reorder packets; an index commits when a majority stores it and the entry belongs to the current term.
  5. 5Committed entries are applied to the state machine (a dictionary with put and del commands); a PUT is answered after the quorum commit, and a linearizable GET waits for applied >= readIndex.
  6. 6A reply goes out only after fsync: term and votedFor are written to state.json (temp file, fsync, rename) and log entries to a WAL with CRC32; a torn tail after a crash is truncated on load.
  7. 7After SNAPSHOT_EVERY applied entries (200 by default) the snapshot of the dictionary is written as a new file generation: a new WAL, then an atomic publish of snapshot.bin, then the old WAL is deleted.
  8. 8The same raft.Node runs in the simulator: time is virtual, the network is controlled (delays, loss, partitions), and a node crash and restart come from its storage; one seed reproduces a run byte for byte, and invariants and linearizability are checked after every step.

Decisions

A core with no goroutines or clocks

raft.Node receives time only through Tick() and messages through Step(), and returns outgoing messages to the caller. So the same code runs in the runtime (25 ms tick, HTTP) and in the simulator, and a failure is reproduced from a seed.

Commit only entries of the current term

An index commits when a majority stores it and the entry belongs to the current term (section 5.4.2 of the paper), so a new leader appends a no-op at once. There is no dedicated test for figure 8, and that is listed in the limitations.

Read-index instead of leases

The leader notes commitIndex, sends a heartbeat, waits for a majority to confirm and only then answers a GET. Correctness does not depend on clocks, and a partitioned leader cannot answer.

Snapshots by generations

A new WAL, then an atomic publish of snapshot.bin (the commit point), then deletion of the old WAL. A crash at any step leaves the whole old or the whole new state.

Trade-offs

  • The membership is fixed (exactly three nodes); there is no dynamic membership, pre-vote or check-quorum, no client sessions or exactly-once: after a 504 the outcome is unknown. A snapshot goes as one message and sits in memory whole, every PUT is a separate fsync, and there is no TLS or authentication.
  • The verification is home-made: a simulator with a perfect disk and a linearizability check on small histories, which is not Jepsen. One hand-made mutation, committing entries of earlier terms, is not caught by the tests.

Tests

One leader per term (40 seeds with loss, crashes and partitions), re-election after the leader dies, replication, no commit without a quorum, rollback of a partitioned leader's tail, restart from disk, no double vote after a restart, snapshots and catch-up, linearizability under chaos (30 seeds), trace determinism; node HTTP tests and the linearizability checker itself; go test -race in CI.

Source: README of the raft-kv project (in Russian), sections "Архитектура" (Architecture), "Как устроены части" (How the parts work), "Инварианты" (Invariants), "Тесты" (Tests), "Чего здесь нет" (What is missing)

10 · auth-service

Keyring: an authentication service

An authentication service on stdlib crypto and SQLite: EdDSA JWTs with key rotation and JWKS, refresh tokens with rotation and reuse detection, TOTP, RBAC, lockout, audit. The point is the token flows and an honest threat model with what stays unaddressed.

  • Go
  • stdlib crypto
  • SQLite
  • modernc.org/sqlite (no CGO)
  • JWT EdDSA/Ed25519
  • JWKS
  • PBKDF2
  • TOTP
  • RBAC
  • REST
  • OpenAPI 3
  • Vanilla JS
  • embed
  • Docker
  • GitHub Actions
  • 50 tests
  • 35 routes
  • 2,826 lines of Go
Component diagram: Keyring: an authentication serviceDiagram of "Keyring: an authentication service": 8 components and 7 links; link numbers match the flow list below.Browsers and clientsBearer · rt cookieThird-party serviceverifies via JWKSHTTP layerCSP · Origin · CSRF · IP limitAuth corePBKDF2 · TOTP · RBACJWT EdDSAkid · key ringSQLitesessions · tokens · keysJWKS/.well-known/jwks.jsonAudit logappend-only1234567
Client / entryServiceStorage

Data flows

  1. 1Sign-in: login and password, with a second step of a TOTP or backup code when 2FA is on; the response carries a 10-minute access JWT, and the refresh token arrives in an HttpOnly SameSite=Strict cookie together with a CSRF token.
  2. 2Middleware checks Origin, CSRF and the per-IP rate, then the access token signature and expiry, then in the database that the session is alive; permissions come from the database on every request, so revocation and role changes take effect at once.
  3. 3Tokens are signed by a ring of Ed25519 keys: only EdDSA is accepted and the kid comes from an allow-list; POST /api/admin/keys/rotate issues a new key, and the old one stays for verification for another access_ttl + 1 minute.
  4. 4The database holds hashes of the password (PBKDF2, 600,000 iterations), refresh tokens (SHA-256) and backup codes; a refresh burns the old token and issues a new one, while presenting a used one revokes the whole family and logs token.reuse_detected.
  5. 5The public keys are served at /.well-known/jwks.json (RFC 7517/8037) along with their kid.
  6. 6A third-party service takes the kid from the JWT header, fetches the key from JWKS (cached for a minute) and verifies the signature, iss, aud, typ and exp; it learns of a session revocation no earlier than the end of the access token's life.
  7. 7Sign-ins, failures, lockouts, 2FA, password resets, session revocations, admin actions, key rotation and permission denials are written to the audit table; database triggers forbid UPDATE and DELETE, and passwords and tokens never reach the log.

Decisions

A database check on every request

The access token is checked against the database (the session is not revoked, the user is not disabled, permissions are current). It costs one SQLite query and in return gives immediate "sign out everywhere", blocking and role changes.

A refresh token family and reuse detection

Every refresh burns the old token and issues a new one. If a used token shows up again it is assumed stolen: the whole session is revoked and both the stolen and the legitimate token are dead.

EdDSA only, and a JWT parser written on the stdlib

The none and HS256 algorithms are rejected before the signature check (alg-confusion defence), the kid comes from an allow-list and the token length is bounded. The tests include HS256 with the public key used as the secret.

The same response and timing for an unknown login

For a nonexistent user a dummy hash is computed, and the response and timing match a wrong password, while forgot always answers 202. One leak remains: registration says the login or e-mail is taken.

Trade-offs

  • The TOTP secret and the private signing keys sit in the database in plain text: with a dump you can forge tokens and 2FA. Passwords use PBKDF2 rather than argon2id, and backup codes a fast SHA-256.
  • It has not been independently audited and has no OAuth/OIDC, WebAuthn or e-mail confirmation; the reset e-mail is a stub in the log. The limiter lives in memory and everything in the SQLite of a single process, and an access token cannot be revoked at third-party consumers before exp.

Tests

JWT (forgery, exp/nbf/iss/aud/typ, alg=none, HS256 with the public key, rotation), TOTP against the RFC 6238 and RFC 4226 vectors, refresh rotation and reuse detection, lockout with exponential delay, 2FA and backup codes, RBAC, password reset, audit (UPDATE/DELETE forbidden), cookie flags, CSRF, CORS, Origin, rate limit; JWKS is checked by independent code; go test -race in CI.

Source: README of the auth-service project (in Russian), sections "Возможности" (Features), "Потоки токенов" (Token flows), "Модель угроз" (Threat model), "Схема БД" (Database schema), "Ограничения" (Limitations)

11 · finance-tracker

Ledger: personal finance tracker

Personal finance tracking: a Go binary with SQLite and an embedded Nuxt 4 front end (an SPA). The point is money in integer minor units, multi-currency with manual rates, a schedule and CSV import, and user isolation.

  • Go
  • SQLite
  • modernc.org/sqlite (no CGO)
  • REST API
  • Nuxt 4
  • Vue 3
  • TypeScript
  • Pinia
  • SVG charts
  • vitest
  • ESLint
  • Docker
  • GitHub Actions
  • PBKDF2
  • CSV import/export
  • 30 tests
  • 45 routes
  • 3,741 lines of Go
Component diagram: Ledger: personal finance trackerDiagram of "Ledger: personal finance tracker": 8 components and 10 links; link numbers match the flow list below.Nuxt 4 SPAPinia · SVG chartsBank CSVimport · exportGo HTTP APIsession cookie · Origin · user_idCSV importpreview · dry runSchedulecron rule · catch-upAccounts and transactionsminor units · big.IntBudgets and reportsmonth · trendsSQLitemodernc · no CGO12345678910
Client / entryServiceStorage

Data flows

  1. 1The browser talks to the API in JSON; the session lives in an httpOnly cookie (SameSite=Lax), the database stores only the SHA-256 of the token, and Origin is checked on write requests.
  2. 2A CSV file and exports (CSV protected against formula injection, and a full JSON) go through the same API, with no separate channels.
  3. 3Every request is bound to the user_id from the session; ordinary CRUD of accounts, categories, rates and transactions goes straight to the accounts and transactions layer, with filters q, kind, account_id, category_id, tag, from and to.
  4. 4POST /api/import/preview detects the delimiter and encoding and shows the column mapping; /commit writes the rows, skips duplicates and returns a report of bad rows (at most 20,000 rows at a time).
  5. 5A cron-like rule of five fields (day precision) is executed when the app is opened and by the /api/recurring/run button; missed dates are caught up, and there is no background scheduler.
  6. 6Imported rows become ordinary transactions: duplicates are skipped, missing categories are created, and transfers are not imported.
  7. 7Recurring payments also become ordinary transactions and keep a recurring_id; the rule’s last_date moves forward.
  8. 8Reports read the transactions whole into memory and compute in the base currency at current rates; the trend slope is a least-squares fit in integers.
  9. 9Amounts are written as integer minor units (amount_minor > 0), a rate is stored as an integer x10^6, conversion goes through big.Int; an account balance is not stored but computed by a query.
  10. 10Budget limits (limit_minor) and warning thresholds (warn_pct) live in the budgets table and are compared with the month’s spending; reports read the same tables and write nothing.

Decisions

Money only in integer minor units

In the database, API and browser amounts are integers of cents, with no floats. Currency conversion goes through big.Int with round-half-away-from-zero, and rounding and amount parsing are covered by tests.

Hand-made SVG charts instead of a library

Only three simple chart types are needed, and a library would add tens of KB of JS. The own ones (about 250 lines) give control over accessibility (role="img", a text table), theming through CSS variables and a strict CSP.

An SPA without SSR

Nuxt is built with nuxt generate and ssr:false because session data cannot be prerendered. Go serves the embedded web/dist and 200.html for unknown paths, and the CSP gets hashes of the inline scripts instead of unsafe-inline for scripts.

Balance is computed, not stored

An account balance = opening balance + income - expenses - outgoing transfers + incoming transfers. There is no stored field, so it cannot drift from the transactions.

Trade-offs

  • Rates are manual and current, with no rate history: the past is recomputed at today's rates. The base currency cannot be changed, a budget is the same every month, and payments are created only when the app is opened.
  • No password change or recovery, SQLite and a single process, and reports read transactions whole into memory. style-src still allows unsafe-inline (Vue styles), and SSR is off.

Tests

Money (parsing, formatting, rounding, conversion, round-trip), budgets, cron rules and catch-up of missed dates, CSV import, user isolation, the API, reports, demo data, TestRealDist (the embedded build is served, the CSP has no unsafe-inline for scripts); on the front end vitest, ESLint and typecheck; go test -race in CI.

Source: README of the finance-tracker project (in Russian), sections "Возможности" (Features), "Архитектура" (Architecture), "Схема БД" (Database schema), "API", "Ограничения" (Limitations)

12 · pixel-wall

Pixel Wall: a shared canvas

A shared 256x256 canvas over WebSocket: an own RFC 6455 on the stdlib, a binary protocol, a snapshot and deltas, a snapshot plus a journal. The point is delta consistency and crash recovery, with every pixel costing 4 bits.

  • Go
  • stdlib only
  • WebSocket
  • Canvas 2D
  • Vanilla JS
  • embed
  • Binary protocol
  • Snapshot + journal
  • Docker
  • GitHub Actions
  • 77 tests
  • 16 routes
  • 2,715 lines of Go
Component diagram: Pixel Wall: a shared canvasDiagram of "Pixel Wall: a shared canvas": 8 components and 8 links; link numbers match the flow list below.BrowserCanvas 2D · zoom · timelapseAdminBearer ADMIN_TOKENHTTP + WebSocketOrigin · limits · 50 ms hubwall coreone mutex · seqCooldown and banssession · IP · rate limitCanvas4 bits per pixelHistory in memoryold and new colorDiskcanvas.snap + journal.log12345678
Client / entryServiceStorage

Data flows

  1. 1The browser opens WS /ws?sid=..., receives a compressed snapshot (0x01: seq, size, the deflate-raw canvas) and then only deltas (0x02: firstSeq and up to 2000 records); a pixel goes out in 6 bytes (0x10: x, y, color).
  2. 2Admin requests carry Authorization: Bearer; the token is compared in constant time, wrong attempts are rate-limited per IP, and without ADMIN_TOKEN the admin API is off (403).
  3. 3The server checks Origin, message sizes (WS up to 64 bytes, body up to 4 KiB) and the signed session, then passes the placement to wall; the hub broadcasts accumulated deltas every 50 ms, and a slow client is disconnected and re-reads the snapshot.
  4. 4Before writing, wall asks the cooldown: strictly one pixel per COOLDOWN_SEC per session and no more than IP_BURST in the same sliding window per IP; a banned session or IP gets 403, an excess 429.
  5. 5A single mutex orders the write to the canvas, the journal and the delta queue; a pixel takes 4 bits (an even index in the high nibble of the byte), 256x256 = 32 KiB.
  6. 6Each placement is appended to the history (up to MAX_HISTORY records) with the old and new color; the client rolls the timelapse back from the current state, so no base snapshot is needed.
  7. 7The canvas.snap snapshot is written every SNAPSHOT_SEC to a temp file with crc32, fsync and rename; recovery takes the snapshot, then the journal records with a larger seq, and truncates a torn tail.
  8. 8journal.log (10 bytes per record: time, x, y, old and new color, clear and bot flags) is appended on every pixel with an fsync every 30 seconds; when MAX_HISTORY is exceeded by 25% the journal is rewritten with its tail.

Decisions

WebSocket instead of SSE and POST

A bidirectional binary channel is needed: a pixel in 6 bytes with an immediate ok or err, and binary deltas. SSE is text (base64 inflates traffic by a third) plus a separate POST per action. A fallback without a socket exists: POST /api/pixel.

A snapshot with seq and deltas with firstSeq

A client registers with the hub and under the same lock gets a snapshot with its seq. A delta carries firstSeq: the client skips records with seq not above its own and reconnects on a gap.

The journal stores the old color

A record "old<<4 | new" lets the client roll history back from the current state, so no base snapshot is needed for the timelapse, and truncating the journal only shortens the history depth.

A snapshot and a journal instead of one file

A snapshot through a temp file and rename guarantees integrity, and the journal gives the latest changes; a corrupt snapshot (crc) is rebuilt from the journal, and a journal that does not join the snapshot starts over.

Trade-offs

  • Everything is in one process on one disk; cooldowns, sessions and rate limits are in memory and reset on restart. A session is an identifier in localStorage, so it guards against simple bots, not an attack from many IPs.
  • The journal has no fsync per pixel (an fsync every 30 s), it is held whole in memory, there are no WebSocket extensions, and the client needs DecompressionStream. There are no browser tests in the repository.

Tests

Cooldown (session, IP, sliding window, table overflow), packing and boundary coordinates, codecs and broken messages, storage (crc, torn tail), recovery with no snapshot and from a snapshot plus a journal, compaction, history rollback, delta continuity, the API (limits, Origin, 401/403/429), WebSocket with two clients and flooding, admin rights, bans, a load of 300 clients; go test -race in CI.

Source: README of the pixel-wall project (in Russian), sections "Возможности" (Features), "Архитектура" (Architecture), "Бинарный протокол" (Binary protocol), "Формат хранения" (Storage format), "Ограничения" (Limitations)

13 · gitpulse

GitPulse: git repository analytics

A CLI for analyzing git repositories: a git log stream parser, metric aggregators and three outputs from one structure: the terminal, a self-contained HTML and a local server. The point is not to write an own .git parser and to trust git with the history and renames.

  • Go
  • stdlib only
  • CLI
  • git
  • html/template
  • SVG
  • ANSI
  • os/exec
  • GitHub Actions
  • Docker
  • 32 tests
  • 3 routes
  • 2,133 lines of Go
Component diagram: GitPulse: git repository analyticsDiagram of "GitPulse: git repository analytics": 9 components and 8 links; link numbers match the flow list below.Command linefour commands and flagsRepository.git · .mailmapgitlog (os/exec)git log -z --numstat -M · streaming parseranalyze.Aggregatorcommit by commit · renames · lock/vendor filteranalyze.Resultone structure · JSONtermANSI tables · sparklinesreporthtml/template · inline SVGreport.htmla single file or stdoutserve127.0.0.1 · Host guard12345678
Client / entryServiceStorage

Data flows

  1. 1The command parses the flags (-rev, -since, -until, -max-commits, -top, -depth, -json, -color) and checks the repository and revision; no git, not a repository or an unknown revision gives exit code 3, bad arguments 2.
  2. 2git reads the history and .mailmap itself; author names therefore arrive already resolved through %aN/%aE, and git -M finds the renames, with no own packfile parser.
  3. 3gitlog runs git log -z --numstat -M through os/exec and parses the stream one record at a time: -z removes quoting and Unicode problems in paths, one commit lives in memory; progress is drawn to stderr on a terminal.
  4. 4The Aggregator takes commits from newest to oldest and accumulates metrics; on meeting old => new it remembers the rename (resolving chains), so earlier edits of old land on the current name; lock files, vendor and min files are skipped, merges are counted separately.
  5. 5The result is a single analyze.Result structure (JSON with -json); term draws tables with colors, bars and sparklines from it, NO_COLOR and output to a pipe or file switch the color off, and on Windows VT and UTF-8 are enabled.
  6. 6report builds HTML through html/template with inline SVG charts, CSS and a small theme-switch JS; there are no external requests, names and paths are escaped, and the theme follows prefers-color-scheme.
  7. 7report writes a single file (-o file, gitpulse-report.html by default) or stdout with -o -.
  8. 8serve accepts only a loopback address in -addr, rejects requests with a foreign Host header (DNS rebinding protection), sets CSP and no-store, keeps the result cached (30 s by default, /?refresh=1 resets it) and serves /, /api/stats and /healthz.

Decisions

git through os/exec, not a .git parser

An own parser of objects and packfiles with delta chains would also need the commit-graph, .mailmap, replace refs, shallow and partial clones and rename detection: thousands of lines with a risk of wrong metrics. git log --numstat -M solves this reliably; the price is a dependency on an installed git, which is checked explicitly.

A -z stream instead of loading the whole log

The log is read one record at a time, memory grows with the number of distinct files and authors, not commits; -max-commits marks the report as truncated.

One Result structure for every output

The terminal, HTML, JSON and the server show numbers from a single analyze.Result, so there is nothing to diverge, and the tests check the metrics in one place.

A self-contained HTML and a loopback server

The report contains author names, so it is a single file with no external requests or fonts, and serve listens only on 127.0.0.1 and checks Host.

Trade-offs

  • Git must be installed; time is the author's UTC; merges are not analyzed; file copies are not tracked and renames are seen only where git -M finds them.
  • Language growth is an estimate of "added minus removed", and bus factor and risk are heuristics over line counts; memory grows with the number of files and authors, -json includes author e-mails and the HTML does not.

Tests

Synthetic repositories built through git and metrics on precomputed data: merges, renames (including an edit by the new path), binary files, Cyrillic in paths and names, lock files, an empty repository, a single commit, the commit limit, ranges, bad revisions; stream parsing (a tab in a path, broken records), a golden test of the HTML structure, self-containment and escaping of the report, the server (cache, Host guard, methods), exit codes, the CLI JSON, NO_COLOR; go test -race in CI.

Source: README of the gitpulse project (in Russian), sections "Команды" (Commands), "Что считается" (What is counted), "Архитектура" (Architecture), "HTML-отчёт" (HTML report), "Тесты" (Tests), "Ограничения" (Limitations); code in internal/gitlog, internal/analyze, internal/report, internal/term, internal/serve