- JavaScript 99.2%
- Dockerfile 0.5%
- HTML 0.3%
| data | ||
| po | ||
| src | ||
| static | ||
| storage | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| .prettierignore | ||
| .prettierrc | ||
| COPYING | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.js | ||
| index.css | ||
| index.html | ||
| index.js | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.js | ||
lanquiz
Host quizzes in LAN from a laptop. Meant to be used over tethered Wi-Fi in a Kyiv school during blackouts. Powered by SQLite, Express, Valibot, Preact, DaisyUI and Vite.
Highly inspired by ClassQuiz. I needed something lighter and easier to set up on different computers, GNU/Linux and Windows alike.
Features
Multiple-choice quizzes
- Import quizzes from Kahoot by URL, or by title plus author nickname.
- A quiz can have multiple-choice questions with up to four answers, multiple can be correct.
- Answers are color-coded and shape-coded. System setting for dark theme is respected.
- A question can have simple HTML formatting, sanitized on server during import.
- True/false questions are supported.
Join by PIN
- The host selects a quiz and starts it by generating a PIN. Users join by entering the PIN to play.
- If player closes tab, they can continue to play by entering the same PIN without losing progress.
- Every user starts with a random emoji and a new editable nickname for each play.
- Players can edit nicknames until the first question is asked.
- The host waits until all users have joined, and asks the first question.
Time limit
- Questions must be answered in time, usually below 1 minute.
- The host and players see how much time is left.
Score with multipliers
- After answering each question, players see the top-3 score and their current score.
- Players don't see what place they're on unless they're in top-3.
- The host's screen shows the top-3 score after each question.
- The faster the player picks the correct answer, the more points they get.
- Also more points for multiple correct answers in a row.
- When all questions have been answered, users can enter their real names if they want.
Live refresh
- New questions appear live, without manually refreshing the page.
- When a new question appears, mobile screen scrolls to top.
Learning from mistakes
- Each player sees the correct answer after picking any or after a question is finished.
- After each finished question, the host can explain the answer and ask the next question.
Themes and images
- A quiz can have a background and a cover. A question can have an image with alt text.
- Fallback is the Lanquiz logo: network-blue by Michal Konstantynowicz.
- Giphy sticker in question is imported as an image.
- Clicking a small image opens a dialog with big image.
- TODO: Question image can be cropped.
- Slides with one image and no answers are supported, they end after 10 seconds.
Editor
Note: Import remains my main usage route.
- The host can edit quiz titles, descriptions, questions, times and answers.
- The host can move a question after another one.
- Setting an answer to empty delete it.
- The host can create or delete local quizes without a corresponding import.
Contributions welcome
Note: correct answers appear in JSON inside the source code. Changing this is not a goal because my quiz sources are public anyway. The point of playing the quizzes is to learn and have fun through timed competition, not to be evaluated.
Testing has been manual so far, using a laptop and a tethered phone. Each feature in the lists above should ideally be covered with an automated test using a one-off API server, Vitest and JSDOM.
I don't think I'll have the spoons to implement mini-game modes, but I'm slightly curious about them because students really enjoy them when playing in Kahoot.
Originally I wanted to implement Lanquiz using cargo-leptos or Dioxus, but setting them up on Windows is much more complicated than I need. I might revisit this in a few months. Or not.
If you'd like to contribute, let's discuss in an issue what we're going to implement, whom it will help and how to maintain it. If you open an issue to ask for a feature, I encourage you to try implementing it yourself; I can answer your questions about app structure.
Install
Don't run Lanquiz on a server with a public IP address. It is only meant for local, trusted networks.
Download the code:
git clone https://codeberg.org/nykula/lanquiz
cd lanquiz
Configure it, changing 192.168.1.2 to your server address in the current LAN:
cp .env.example .env
echo PORT=8000 >>.env
echo ORIGIN=http://192.168.1.2:8000 >>.env
Option 1: Container. Download and run a prebuilt container (~150 MB) using the configuration above:
podman compose --env-file=.env up --detach
If you want to use the container without compose, the latest version is available as docker.io/nykula/lanquiz:latest. It exposes port 8000. Set environment variable ORIGIN=http://192.168.1.2:8000 using your IP address.
Option 2: Own Node.js. Download dependencies (~300 MB), then build and run Lanquiz using your own Node.js (tested with v24):
npm install
npm run build
npm start
If you use Termux, instead of npm install, try:
pkg install -y binutils-is-llvm build-essential
GYP_DEFINES=android_ndk_path= npm install
Development
You MUST make the source code of your changes available to your users.
Start a server with hot reload:
npm run dev -- --open
Lint the code and make sure it builds:
npm run format && npm run build
Analyze bundle size in dist/stats.html.
Run tests:
npm test
(Reminder for myself) Publish the latest container for amd64 and arm64:
apt install -y qemu-user-static
podman manifest create docker.io/nykula/lanquiz
podman build --platform linux/amd64,linux/arm64 --manifest docker.io/nykula/lanquiz .
podman login docker.io
podman manifest push docker.io/nykula/lanquiz
Translation
Create an empty file po/LANGUAGE.po (find your two-letter language code first) and run:
npm run extract
Then translate po/LANGUAGE.po using Poedit. Add your language to acceptLanguages in src/lib/index.js. Send your translation as a merge request.
License
SPDX-License-Identifier: AGPL-3.0-or-later
Copyright (C) 2025-2026 Denys Nykula
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
EviKit
Framework parts of this codebase are under a more permissive MIT license.