Self-hosted Q&A and discussion companion for events and conferences.
Event-Q&A displays the program of an event (tracks, talks, rooms,
speakers) and lets attendees ask and discuss questions per talk,
browse other attendees, and exchange direct messages. To run an event,
the admin writes a single event.yaml describing the schedule and
launches the tool on a server. Participants connect from any device
using a self-chosen alias.
- Schedule view — timeline grid on desktop, chronological list on mobile, grouped by track and day.
- Per-talk Q&A — attendees post questions, upvote them, and discuss in a flat chatroom under each question.
- Direct messages — simple two-party chat between any two attendees.
- Lightweight auth — claim an alias on first visit; optional password to protect it across devices.
- Admin moderation — hide questions/messages, mark questions as answered, ban or rename users.
- Realtime updates — new questions, votes, replies and DMs arrive over WebSocket without reload.
- Dark mode — follows OS preference, with a per-user override.
- Single config file — everything (schedule, rooms, speakers,
admins) lives in one
event.yaml. - Single container — backend, frontend and SQLite are served by one Docker image; one mounted volume holds all state.
Requires Docker and Docker Compose.
# 1. Clone the repo and copy the example config into ./data, the
# directory that gets mounted into the container
git clone https://github.com/c4dt/event-qa.git
cd event-qa
mkdir -p data
cp event.example.yaml data/event.yaml
$EDITOR data/event.yaml
# 2. Start the stack
docker compose up -d
# 3. Put a TLS-terminating proxy (traefik, Caddy, nginx) in front of
# port 3000.The server listens on port 3000 and stores state in ./data
(event.yaml and the SQLite database).
To generate an argon2id password hash for an admin entry:
docker run --rm -it ghcr.io/your-org/event-qa hash-passwordThe admin maintains a single yaml file, event.yaml, mounted into the
container at /data/event.yaml. A minimal example:
event:
name: "My Conference 2026"
place: "EPFL, Lausanne"
start: 2026-06-01
end: 2026-06-02
info: |
Welcome to the conference. Wi-Fi: guest / hunter2.
rooms:
- id: main
name: "Main Hall"
description: "Ground floor, capacity 300"
- id: bh1
name: "Breakout 1"
speakers:
- id: ada
name: "Ada Lovelace"
bio: "Mathematician, programmer."
admins:
- alias: admin
password: "$argon2id$..." # see "Quick start" for how to hash
tracks:
- id: main-track
name: "Main Track"
talks:
- id: opening
title: "Opening keynote"
start: 2026-06-01T09:00:00+02:00
duration_min: 45
speakers: [ada]
room: main
# qa: true # optional, default true. Set to false for breaks/socials.
- id: lunch
type: slot # non-talk entry: no speaker, no Q&A
title: "Lunch"
start: 2026-06-01T12:00:00+02:00
duration_min: 90
room: mainThe config is re-read periodically (every 5 minutes by default), so
editing data/event.yaml takes effect without restarting the container.
EVENT_CONFIG can also point at an http(s):// URL instead of a local
file — e.g. a github.com link to event.yaml in its own repo, which is
auto-converted to the raw file URL — so an admin can update the event by
editing that file rather than touching the deployment at all. The full
schema, validation rules, and EVENT_CONFIG/EVENT_CONFIG_REFRESH options
are documented in
TECHNICAL.md.
A complete sample is available at
event.example.yaml.
All tooling (Node, pnpm, sqlite, argon2 CLI) is pinned via devbox — do not rely on system-installed Node or pnpm.
# Enter the devbox shell
devbox shell
# Install workspace dependencies
pnpm install
# Run backend (:3000) and frontend (:5173) dev servers
pnpm dev
# Run tests (red/green TDD)
pnpm testOutside the devbox shell, prepend every command with devbox run --.
See TECHNICAL.md for more details on the
toolchain, the repository layout, and the test strategy.
- USER_GUIDE.md — what attendees and admins see and do (login, schedule, Q&A, DMs, moderation).
- TECHNICAL.md — architecture, data model, HTTP and WebSocket APIs, deployment, security notes.
event.example.yaml— annotated config sample.
| Layer | Choice |
|---|---|
| Backend | Node.js (>=20) + Express + TypeScript |
| Realtime | WebSockets via ws |
| Frontend | SvelteKit + TypeScript + Tailwind CSS |
| Persistence | SQLite via better-sqlite3 |
| Passwords | argon2 (argon2id) |
| Config | YAML (js-yaml) validated with zod |
| Container | Single multi-stage Dockerfile, run via docker-compose |
| Tests | vitest for both backend and frontend |
Issues and pull requests are welcome. Before opening a PR:
- Read TECHNICAL.md to understand the architecture.
- Run
pnpm lintandpnpm testfrom the devbox shell. - For backend logic (auth, rate limiting, admin actions), follow red/green TDD: write the failing test first, then the code.
Licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See LICENSE for the full text.