A no-signup group date/time picker. One link, everyone marks when they can, and Kogda ranks the slot that works for the most people. No accounts, no app.
- Backend: FastAPI + aiosqlite (a single SQLite file, no ORM).
- Frontend: buildless — Tailwind Play CDN + vanilla JS, served as static files.
- Realtime: in-process WebSocket hub broadcasts a
changedsignal on every mutation; clients refetch. Single-process uvicorn.
src/kogda/
main.py FastAPI app, mounts the frontend last
core/config.py pydantic-settings (KOGDA_ env prefix)
core/db.py aiosqlite schema init
core/realtime.py EventHub WebSocket pub/sub
api/events.py thin router -> service
schemas/event.py pydantic request models (Field caps)
services/event_service.py store + orchestration, owns commits + status guards
services/ranking.py pure best_slot() ranking, no I/O
frontend/ index.html + app.js + styles.css
tests/ pytest (api, best_slot, status guards)
- Event —
code(short slug),title,status(open|finalized),chosen_option_id. Soft-deletable. - Option — a candidate slot (
label, e.g. "Mon 15 July, 19:00"). - Participant — a pseudonym, no PII.
- Availability — a participant's 3-state vote per slot:
yes/if_need/no.
best_slot() scores each slot yes * 2 + if_need; the top score wins, ties
break on more firm yeses, then the earliest slot.
uv venv
uv pip install -e ".[dev]"
uv run uvicorn kogda.main:app --reload
# tests
uv run pytest -qOpen http://localhost:8000.
docker compose up -d. The SQLite file lives on an external named volume
kogda-data mounted at /data, with KOGDA_DB_PATH=/data/kogda.db, so redeploys
never wipe events. Create the volume once before the first deploy:
docker volume create kogda-dataPOST /api/v1/events{title}->{id, code, title}GET /api/v1/events/{code}-> event + options + participants + availability + standingsPOST /api/v1/events/{code}/participants{name}POST /api/v1/events/{code}/options{label, added_by?}(open only)DELETE /api/v1/options/{option_id}(open only)PUT /api/v1/options/{option_id}/availability{voter, state}(open only)PATCH /api/v1/events/{event_id}/finalize{option_id}POST /api/v1/events/{event_id}/reopenDELETE /api/v1/events/{event_id}+POST /api/v1/events/{event_id}/restoreWS /api/v1/events/{code}/ws— livechangedsignal