No description
Find a file
nzambello 5d45774dae
All checks were successful
Docker CI / check (push) Successful in 38s
Docker CI / audit (push) Successful in 6s
Docker CI / release (push) Successful in 50s
chore: update zod version
2026-09-01 15:12:52 +02:00
.agents/skills/hallmark chore: add hallmark skills 2026-08-05 10:24:16 +02:00
.cursor/rules initial commit 2026-08-05 10:11:14 +02:00
.forgejo/workflows ci: publish tagged image from git tags 2026-08-05 12:22:20 +02:00
src feat: show months in counter durations 2026-08-05 14:49:41 +02:00
.dockerignore chore: use official oven/bun docker image and bun user 2026-08-05 10:48:27 +02:00
.editorconfig fix: rename .editorConfig to .editorconfig so CI picks up CSS indent 2026-08-05 11:50:21 +02:00
.env chore: refactor docker build 2026-08-05 10:49:19 +02:00
.env.example chore: docker image for compiled bun binary 2026-08-05 10:44:31 +02:00
.gitignore chore: docker image for compiled bun binary 2026-08-05 10:44:31 +02:00
.oxfmtrc.json chore: format codebase 2026-08-05 11:36:45 +02:00
.oxlintrc.json initial commit 2026-08-05 10:11:14 +02:00
.release-it.json ci: no gh release 2026-08-05 12:23:18 +02:00
bun.lock chore: update zod version 2026-09-01 15:12:52 +02:00
bunfig.toml feat: handle timezone correctly, add e2e tests 2026-08-05 12:37:01 +02:00
CHANGELOG.md chore: release v1.0.0 2026-08-05 15:14:16 +02:00
commitlint.config.mjs initial commit 2026-08-05 10:11:14 +02:00
docker-compose.yml refactor: fix permissions over volumes 2026-08-05 14:17:04 +02:00
Dockerfile refactor: fix permissions over volumes 2026-08-05 14:17:04 +02:00
lefthook.yml chore: add test/typecheck scripts and docs changelog type 2026-08-05 10:31:03 +02:00
package.json chore: update zod version 2026-09-01 15:12:52 +02:00
PROJECT.md refactor: rename routes for shorter paths 2026-08-05 12:19:07 +02:00
README.md feat: client-side counter, remove htmx 2026-08-05 14:23:32 +02:00
skills-lock.json chore: add hallmark skills 2026-08-05 10:24:16 +02:00
tsconfig.json feat: hono app shell and env validation 2026-08-05 10:32:21 +02:00

Counter

Self-hosted countdown / count-up app. Public share URLs for counters and views; admin CRUD behind a shared key.

Stack

  • Bun + bun:sqlite
  • Hono (routing, middleware, JSX HTML)
  • Vanilla CSS + small client scripts (local datetime, counter ticker)
  • Zod + t3-env

Setup

bun install
cp .env.example .env   # set ADMIN_KEY
bun run dev            # http://localhost:3000

Environment

Variable Default Description
PORT 3000 HTTP port
ADMIN_KEY (required) Shared admin password
DATABASE_PATH ./data/counter.db SQLite file path
NODE_ENV development Enables Secure cookies when production
ASSETS_ROOT auto Override static assets directory

Scripts

bun run dev        # hot reload
bun run start      # production-ish local run
bun test           # unit + snapshot + e2e
bun run test:e2e   # app flow e2e only
bun run build      # sync assets + compile binary to dist/counter
bun run lint
bun run format

Routes

Path Description
/ Homepage: counters with show_in_homepage
/c/:uuid Counter detail (works for private counters)
/v/:uuid Ordered collection of counters
/admin/ Redirects to login or counters
/admin/auth Shared-key login
/admin/counters Counter CRUD
/admin/views View CRUD

Docker

Follows the official Bun Docker guide. The image compiles a standalone executable with bytecode, minify, and sourcemaps (oven/bun, default bun user).

Published image (see docker-compose.yml):

mkdir -p ./data && sudo chown -R 1000:1000 ./data
docker compose up -d

Or build locally:

docker build --pull -t counter-app .
docker run --rm -p 3000:3000 \
  -e ADMIN_KEY=change-me \
  -v counter-data:/data \
  counter-app

The container serves on port 3000 and stores SQLite at /data/counter.db. Static assets live beside the binary under /usr/src/app/public.

Data directory permissions

The container runs unprivileged as uid 1000 (bun) and never escalates. SQLite writes counter.db plus its -wal / -shm siblings, so the mounted directory must be writable by that uid:

  • Named volume — works out of the box; Docker seeds ownership from the image.
  • Bind mount — keeps host ownership, so chown -R 1000:1000 <host dir> first, or set a matching user: in docker-compose.yml and own the directory with that uid.

If it isn't writable the app exits at startup with an explicit message naming the directory and the uid it ran as, rather than a bare SQLITE_CANTOPEN.