Host quizzes in LAN from a laptop. (Import from Kahoot. Self-host during blackouts.)
  • JavaScript 99.2%
  • Dockerfile 0.5%
  • HTML 0.3%
Find a file
2026-02-15 04:06:24 +02:00
data Move local.db to data/ for easier backups using podman volume 2025-12-16 12:27:26 +02:00
po Fix Question %1 message in Ukrainian 2026-01-19 03:33:14 +02:00
src Thumbs up when not top 3 2026-02-15 04:06:24 +02:00
static Reduce dependencies, no SvelteKit 2025-12-13 03:58:02 +02:00
storage Keep images in static storage, without database interaction 2025-12-26 21:20:47 +02:00
.dockerignore Publish container 2025-12-29 17:16:49 +02:00
.env.example Hot-reload server routes in dev mode (move from vite-express to router import in vite.config.js) 2025-12-14 00:36:08 +02:00
.gitignore Make a node:sqlite ORM with Valibot schema, switch from Drizzle 2026-02-06 02:53:14 +02:00
.npmrc Initial commit 2025-12-11 09:03:01 +02:00
.prettierignore Make a node:sqlite ORM with Valibot schema, switch from Drizzle 2026-02-06 02:53:14 +02:00
.prettierrc Return trailingComma=none to prettierrc because I get some conflict of eslint and prettier 2025-12-15 20:17:53 +02:00
COPYING Initial commit 2025-12-11 09:03:01 +02:00
docker-compose.yml Lint quotes in docker-compose.yml 2025-12-29 22:23:13 +02:00
Dockerfile Make a node:sqlite ORM with Valibot schema, switch from Drizzle 2026-02-06 02:53:14 +02:00
eslint.config.js Apply ts.configs.strict and ts.configs.stylistic eslint rules 2026-01-11 21:13:56 +02:00
index.css Theme quiz with DaisyUI. Node can import routes directly now 2025-12-17 18:07:02 +02:00
index.html +page -> page, src/index.js -> src/app.js 2026-01-21 02:02:12 +02:00
index.js +page -> page, src/index.js -> src/app.js 2026-01-21 02:02:12 +02:00
package.json Make a node:sqlite ORM with Valibot schema, switch from Drizzle 2026-02-06 02:53:14 +02:00
README.md Make a node:sqlite ORM with Valibot schema, switch from Drizzle 2026-02-06 02:53:14 +02:00
tsconfig.json Comply with stricter tsc defaults 2026-01-14 06:32:44 +02:00
vite.config.js Switch form parser to busboy and make EviKit compatible with node:http, reducing dependency on express 2026-01-19 03:33:14 +02:00

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.