Architecture¶
Targetlog is one Go server with the SvelteKit app embedded, backed by PostgreSQL, built to run as stateless replicas on Kubernetes.
flowchart LR
B[Browser SPA] -- REST / WebSocket --> S[targetlog server]
K[Kiosk screen] -- REST / WebSocket --> S
A[Print agent] -- long poll HTTPS --> S
S -- pgx --> P[(PostgreSQL)]
P -- LISTEN/NOTIFY --> S
Repository layout¶
| Path | Content |
|---|---|
cmd/targetlog |
server entry point and janitors; targetlog openapi prints the API spec |
cmd/print-agent, internal/agent |
the print agent |
internal/domain |
rules without storage or HTTP: age categories, programme definitions, registration rules, results and awards, ranking, club competition, special prizes, lane syntax, amount in words |
internal/store |
handwritten pgx store; migrations/*.sql embedded and applied at startup |
internal/web |
HTTP: health checks, huma API under /api, admin panel, SPA fallback |
internal/docs |
PDF rendering with go-pdf/fpdf |
internal/i18n |
gettext catalogs for documents and exports |
internal/realtime |
WebSocket hub fed by PostgreSQL NOTIFY |
web/ |
SvelteKit 2 SPA (Svelte 5, Tailwind, TanStack Query, shadcn-svelte) |
e2e/ |
Cypress specs |
helmchart/, deploy/ |
Helm chart and values per environment |
docs/ |
this documentation |
Tenancy¶
A tenant is a club. Every tenant-scoped store method takes the tenant ID and every query filters on it; there is no row-level security. API paths are /api/tenants/{tenantId}/… and every handler first calls s.access(ctx, tenantID, adminOnly), which answers 404 to non-members. Join tables carry tenant_id with composite foreign keys. internal/store/tenants_test.go checks the isolation and must grow with every new table.
Authentication¶
Global users with bcrypt passwords; sessions in PostgreSQL keyed by the SHA-256 of the cookie token (targetlog_session, sliding 7 days, at most 30). Kiosk links and print agents use their own hashed tokens and never create a session. The admin panel uses a stateless HMAC-signed cookie scoped to /admin.
Realtime¶
Handlers call s.notify(ctx, tenant, event, topics…) after a change, which runs pg_notify. Every replica holds one LISTEN connection and forwards the topics to the WebSocket clients of that tenant and event. The frontend (web/src/lib/live.svelte.ts) invalidates TanStack queries by topic; all event-scoped query keys start with ['tenants', t, 'events', e], so a reconnect simply invalidates that prefix.
Documents¶
internal/docs renders PDFs with fpdf's core Helvetica (cp1252), QR codes and Interleaved 2 of 5 barcodes. Reports are generic tables (docs.Report) rendered as PDF or CSV; a new report is a new case in eventData.report in internal/web/reports.go.
Printing¶
print_jobs is a queue in PostgreSQL. Agents long-poll GET /api/agent/jobs/next, which claims a job with FOR UPDATE SKIP LOCKED, and confirm it with POST /api/agent/jobs/{id}.