Synchronized timer for NSF 25m shooting competitions.
Install it on any phone, tablet, or computer and run competitions across multiple devices.
- Built-in NSF programs — Fine/Heavy Pistol, Standard, Silhouette, NAIS, Rapid Fire with full stage/series timing
- Custom programs — Create, edit, and delete your own competition programs
- Multi-device sync — Host creates a room, shooters join by 4-character code. Timer state syncs in real-time via WebSocket
- Precision, Rapid & Duel modes — Countdown timers for precision stages, hidden/visible cycling for rapid-fire and duel stages
- Malfunction handling — Track jams per shooter (max 2 per program, 1 per stage) with reshoot support
- Solo mode — Run a full competition program locally without creating a room
- Spectator mode — View-only mode for spectators to follow the competition timer
- Bilingual — Norwegian and English
- Offline-capable PWA — Install to home screen, works without internet after first load
- Standalone stopwatch — Simple stopwatch mode independent of the competition system
- Settings — Text scaling, sound toggle, screen wake lock, countdown format
- Host creates a room and selects a shooting program
- Shooters join with the room code, their name, and lane number
- Host controls the timer — start, pause, stop, reset
- All connected devices display the synchronized countdown in real-time
The Docker image bundles the Svelte frontend and WebSocket relay server into a single container. Nginx serves the static files and proxies /ws to the Node.js relay server internally.
docker run -d -p 80:80 ghcr.io/gilbn/nsf-timer:latestAccess the app at http://<server-ip>/.
For internet-facing deployments, restrict WebSocket origins:
docker run -d -p 80:80 \
-e WS_ALLOWED_ORIGINS=https://timer.example.com \
ghcr.io/gilbn/nsf-timer:latestFor HTTPS, place the container behind a reverse proxy (e.g. Caddy, Traefik, nginx Proxy Manager) that handles TLS termination. The app automatically upgrades to wss:// when served over HTTPS.
Note: PWA features (install to home screen, offline support, screen awake) require HTTPS. The app works over plain HTTP, but browsers will not offer the install prompt or register the service worker without a secure context.
services:
nsf-timer:
image: ghcr.io/gilbn/nsf-timer:latest
ports:
- "80:80"
restart: unless-stopped
environment:
WS_ALLOWED_ORIGINS: "*"Below are minimal examples for putting the container behind a reverse proxy with HTTPS. The key requirement is that the /ws path must proxy WebSocket connections (HTTP Upgrade header).
Caddy
timer.example.com {
reverse_proxy nsf-timer:80
}
Caddy handles HTTPS automatically and proxies WebSocket upgrades out of the box — no extra configuration needed.
Nginx
server {
listen 443 ssl;
server_name timer.example.com;
ssl_certificate /etc/ssl/certs/cert.pem;
ssl_certificate_key /etc/ssl/private/key.pem;
location / {
proxy_pass http://nsf-timer:80;
}
location /ws {
proxy_pass http://nsf-timer:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Traefik (Docker labels)
services:
nsf-timer:
image: ghcr.io/gilbn/nsf-timer:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.nsf-timer.rule=Host(`timer.example.com`)"
- "traefik.http.routers.nsf-timer.entrypoints=websecure"
- "traefik.http.routers.nsf-timer.tls.certresolver=letsencrypt"
- "traefik.http.services.nsf-timer.loadbalancer.server.port=80"
environment:
WS_ALLOWED_ORIGINS: "https://timer.example.com"Traefik handles WebSocket upgrades automatically.
| Variable | Default | Description |
|---|---|---|
VITE_WS_SERVER_URL |
/ws |
WebSocket server URL. Relative paths are resolved against the current host at runtime |
VITE_BASE_PATH |
/ |
Base path for the app (e.g. /timer/ if hosted under a subpath). The Docker image assumes / |
VITE_HTTPS |
false |
Set to true to enable HTTPS on the Vite dev server |
| Variable | Default | Description |
|---|---|---|
WS_PORT |
8080 |
Port the WebSocket relay server listens on |
WS_ALLOWED_ORIGINS |
* |
Comma-separated list of allowed origins for WebSocket connections |
npm install
npm run devOpen http://localhost:5173 in your browser.
The dev server proxies /ws to localhost:8080. Start the WebSocket relay server in a separate terminal:
cd server && npm install && npm run dev- Svelte 5 — UI framework
- Vite — Build tool with HMR
- vite-plugin-pwa — Service worker & manifest generation
- ws — WebSocket relay server (Node.js)
- Nginx — Static file serving & WebSocket proxy (Docker)



