A real-time presence "radar" web app — see who else is online and ping them with a click. Single Go binary, SQLite-backed, server-side rendered with templ, live-updated over Server-Sent Events.
┌──────────────┐
│ browser │
│ (HTMX + SSE) │
└───────┬──────┘
│ HTTPS / SSE
▼
┌──────────────┐
│ biatosh │ single binary
│ (Fiber v3) │ no CGO, ~16MB static
└───────┬──────┘
│ SQL
▼
┌──────────────┐
│ SQLite │ modernc.org pure-Go driver
└──────────────┘
# Generate (templ + sqlc) and build a CGO-free binary
make generate
CGO_ENABLED=0 go build -ldflags="-w -s" -o biatosh .
# First run: migrate, seed system roles + 8 demo users, serve
./biatosh migrate up
./biatosh seed dev
./biatosh serve
# Visit http://127.0.0.1:8181 and log in as ali/ali (or any of:
# amin, reza, behzad, omid, sepi, yasin, masih — password = username).For live-reload during development:
make dev # runs `air`, which re-runs `templ generate` and `sqlc generate` before every buildmake build # docker build -t biatosh:latest .
make run-docker # runs container on host port 8181, data volume at /dataThe container automatically applies migrations and seeds system roles at startup. Demo users are NOT seeded in the container build — call biatosh seed dev explicitly if you want them.
All settings come from environment variables (see internal/config/config.go).
| Env var | Default | What it does |
|---|---|---|
BIATOSH_HTTP_IP |
127.0.0.1 |
bind address |
BIATOSH_HTTP_PORT |
8181 |
bind port |
BIATOSH_DB_PATH |
db.sqlite |
SQLite file path |
BIATOSH_SESSION_TTL |
168h |
how long an issued session is valid |
BIATOSH_SESSION_COOKIE |
biatosh_sid |
cookie name |
BIATOSH_SESSION_SECURE |
true |
flip to false for plain-HTTP local dev |
BIATOSH_SESSION_SAMESITE |
Lax |
cookie SameSite policy |
BIATOSH_LOG_LEVEL |
info |
debug / info / warn / error |
--ip, --port, --db-path CLI flags override the corresponding env vars.
biatosh serve # HTTP server
biatosh migrate up|down|status # goose-managed schema
biatosh seed system # idempotent role + permission catalog
biatosh seed dev # system roles + 8 demo users
biatosh user create --username ... --password ... --email ... --name ...
biatosh user list
biatosh user grant <username> <role-slug> # role-slug is "admin" or "user"
biatosh user revoke <username> <role-slug>
Layered with consumer-side interfaces. The compile-time rule: anything in internal/transport/* may NOT import internal/repository/* directly — handlers must go through services.
cmd/ # cobra entrypoints, no business logic
main.go # signal-aware ctx → cmd.New().Execute()
internal/
app/container.go # composition root; wires services
config/ # typed env-var Config
contract/errors.go # sentinel errors crossed across services
domain/
user/ # User type
auth/ # Session, Role, Permission, GroupRole
migrations/ # //go:embed *.sql; goose-managed
observability/ # *slog.Logger, /healthz, /readyz, /metrics
platform/
crypto/ # argon2id wrapper
ids/ # UUIDv7 generator
service/
user/ # Register, Authenticate, Get, List
auth/ # IssueSession, RBAC (Can, CanInGroup), SeedRolesAndPermissions
audit/ # Record(action, actor, target, ...)
presence/ # in-memory Hub for SSE roster + notify
repository/sqlite/ # adapters; sqlc-generated queries in database/
transport/http/
http.go # Fiber v3 app wiring
router/ # mounts /, /login, /events, /notify/:target
middleware/ # requestid, logger (slog), csrf, authn, authz, ctx
controller/ # LoginPage, LoginSubmit, Dashboard, Logout, Notify
sse/ # GET /events SSE handler
render/ # templ → fiber.Ctx adapter
web/ # templ component tree
layout/, page/, component/, partial/
- Browser logs in → server issues a
sessionsrow, sets opaquebiatosh_sidcookie. - Browser opens
GET /events(Server-Sent Events). The handler registers apresence.Streamkeyed by user id and broadcasts the new roster to everyone. - Server-rendered HTML fragments (rendered by templ partials) are pushed as named SSE events:
event: radar→ HTMXsse-swap="radar"replaces the#radar-usersdivevent: toast→ HTMX appends a toast notification to#toasts
- Clicking a dot fires
POST /notify/:targetID; the server finds that user's SSE stream and pushes a toast. - Disconnect (browser close, tab close) cancels the stream context → hub deregisters → broadcasts again.
There's exactly one piece of custom JS (~10 lines, inline in radar.templ): a htmx:sseMessage listener that fires Notification API toasts as a desktop notification on top of the in-page toast.
/healthz— liveness (always 200 if the process can serve)/readyz— readiness (200 only when the DB pings successfully)/metrics— Prometheus text format: request counters by status class, goroutine count, allocated memory, uptime- structured slog JSON logs to stderr, level via
BIATOSH_LOG_LEVEL - every request gets an
X-Request-ID(UUIDv7); it's attached to access log lines
The full plan lives at /home/farshad/.claude/plans/glistening-whistling-pond.md. All six phases have shipped:
- ✅ Phase 1 — Foundation:
internal/skeleton,app.Container, typedconfig, signal-aware main. - ✅ Phase 2 — Persistence: goose migrations, pure-Go SQLite, real
time.Timecolumns. - ✅ Phase 3 — Schema: UUIDv7 PKs, argon2id passwords, RBAC + sessions + groups + audit.
- ✅ Phase 4 — Service layer: per-service consumer-side repositories, server-side revocable sessions,
*slog.Logger, custom CSRF, authn/authz middleware. - ✅ Phase 5 — Fiber v3 + templ + SSE: HTMX-over-SSE replaces WebSocket; presence hub interface; no HTML files outside
internal/web/. - ✅ Phase 6 — Observability + polish:
/healthz//readyz//metrics,biatosh user list, README.