| .agents/skills/hallmark | ||
| .cursor/rules | ||
| .forgejo/workflows | ||
| src | ||
| .dockerignore | ||
| .editorconfig | ||
| .env | ||
| .env.example | ||
| .gitignore | ||
| .oxfmtrc.json | ||
| .oxlintrc.json | ||
| .release-it.json | ||
| bun.lock | ||
| bunfig.toml | ||
| CHANGELOG.md | ||
| commitlint.config.mjs | ||
| docker-compose.yml | ||
| Dockerfile | ||
| lefthook.yml | ||
| package.json | ||
| PROJECT.md | ||
| README.md | ||
| skills-lock.json | ||
| tsconfig.json | ||
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 matchinguser:indocker-compose.ymland 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.