For people who take part in many hackathons and the administrators who line them up: assignments, stages, deadlines and reminders in one place, so a registration window never closes while you were looking the other way.






The participant's event card on a local stack. The badge reads You marked it, and the
line under the form repeats why: the system does not check the external site.
Timeline never checks the external registration site. A registration status is the participant's own report, or a state an administrator set, never a confirmed fact.
The interface says so in as many words: "you marked it", not "confirmed". The status carries
its source (participant, administrator or system) in its own column, a bulk change by an
administrator is limited to TODO and NOT_REQUIRED, and the move from NOT_REQUIRED to
DONE is refused with 409 invalid_state_transition instead of being applied quietly.
An administrator creates an event, assigns participants and configures reminders. A participant sees what they are assigned to, and marks for themselves that they have registered. Reminders go out over Telegram or email.
Participants also form standing teams and enter an event with a lineup drawn from one; each keeps a private layer of tasks, notes, submissions, an outcome and a retrospective against every event they took part in.
- Create. An administrator adds an event: period, format, tracks and prizes, team size, and the stages it runs through.
- Assign. Participants are added in a batch, each with a team role. Every item is its own transaction, so a partial failure still reports per row.
- Set the deadline. A registration carries the organiser's link and a deadline, which may never fall after the event ends.
- Schedule reminders. Up to ten rules per registration, each an offset before the deadline. The scheduler turns them into notifications, one per business situation: a versioned logical key drops the repeat.
- Report. The participant opens the organiser's form, registers there, and marks it here.
- Deliver. The worker takes notifications under a lease and sends them to Telegram or email.
%%{init: {"theme":"base","themeVariables":{"primaryColor":"#ffffff","primaryTextColor":"#1e1a14","primaryBorderColor":"#ac4600","secondaryColor":"#efeae5","tertiaryColor":"#faf7f3","lineColor":"#665f56","textColor":"#1e1a14","fontFamily":"Geist"}}}%%
flowchart LR
SPA["React SPA"] --> Edge["nginx, one origin"]
Edge --> API["Go API<br/>controllers"]
API --> SVC["services<br/>domain rules"]
SVC --> REPO["adapters<br/>SQL over pgx"]
REPO --> DB[("PostgreSQL")]
SCHED["scheduler"] --> DB
DB --> WORKER["worker<br/>lease and retry"]
WORKER --> TG["Telegram"]
WORKER --> MAIL["email"]
The API and the SPA are served from one origin, so the session cookie needs no CORS and no
SameSite=None. The scheduler and the worker are one separate process beside the API,
reading the same database.
Everything runs in containers: Go, Node and PostgreSQL are not needed on the machine, Docker is enough.
make devThe application comes up on http://localhost:5173, the API on http://localhost:8080, and
Swagger UI on /docs/. Use make docker-up instead for the production build behind nginx,
and make help for everything else.
No account is created automatically. Create the first administrator with the backend CLI, then sign in with it:
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml \
run --rm --no-deps backend go run ./cmd/timeline admin create \
--email you@example.com --password 'a-long-password' --name 'Your Name' --nickname youConfiguration, Telegram and checks
Configuration comes only from environment variables; .env.example is the
full list, and make docker-up creates .env from it.
Telegram setup and connection diagnostics: Telegram deployment guide.
Before a change counts as done, make verify has to pass: the contract, types, linters and
tests for both parts. make test-contract runs the contract tests against the real API in a
container, and make e2e signs in through a real browser against the same stack.
A modular monolith in Go behind a React SPA, with a separate worker process for delivery.
backend/ Go: cmd, internal/{config,models,services,adapters,controllers,cli}, migrations
frontend/ React + TypeScript + Vite, Tailwind + shadcn/ui, TanStack Query, react-router
deploy/ Dockerfiles, nginx.conf, docker-compose.yml (+ .dev.yml)
docs/ The generated OpenAPI contract, ratchet baselines and these illustrations
scripts/ Ratchet checks and the README renderer, called from the Makefile
Inside the backend the layers are strict: controllers know about HTTP, services hold
domain rules and transaction boundaries, adapters/repositories is SQL over pgx, and
models is domain types and errors. A controller never reaches into a repository directly.
The frontend is organised by domain: src/entities for the things themselves,
src/features for what is done with them, shared code in src/shared, tokens in
src/styles, and the shell and routes in src/app.
The API contract lives in docs/swagger.yaml, generated from
annotations beside the controllers and never edited by hand. It is written before the
implementation, so both halves proceed from one description.
Several rules are enforced by machine rather than by review: a 250-line limit per file and a
file-count limit per package, both as ratchets over a recorded baseline that only moves down;
no import cycles, counting test imports too; no Cyrillic outside the localisation
dictionaries; and images only under screenshots/, frontend/public, frontend/src/assets
or docs/. The banner, buttons and badges above are rendered from the application's own
tokens, font and mark by make readme.
- A registration status is not a fact. Nothing queries the organiser's site, so a
participant who marks
DONEwithout registering staysDONEuntil someone corrects it. - Delivery is at least once, not exactly once. The worker claims notifications with
FOR UPDATE SKIP LOCKEDunder a lease; a worker that dies after sending and before recording the send leaves the job to the next one, and the reminder arrives twice. - Three defaults turn protection off so that
http://localhostneeds no setup, and all three must change on deployment.COOKIE_SECURE=falselets session cookies travel over plain HTTP; an emptyMETRICS_TOKENleaves/metricsopen to anyone who can reach the port; an emptySMTP_HOSTwrites mail to the log instead of sending it. The backend warns about each one at start.
Team GoatWhistle, 2026. Licensed under MIT. Geist and Geist Mono are used under the SIL Open Font License.
















