No description
  • Go 79.4%
  • JavaScript 9.4%
  • CSS 7.7%
  • HTML 3.5%
Find a file
Arne Skaar Fismen 3ba4010618 internal/web/static: start live.js bridge even if DOMContentLoaded fired
The bridge installer was gated on a DOMContentLoaded listener. On a
back-forward cache restore (and any other path where the body / script
state outlives the original parse), DOMContentLoaded never fires
again, so installBridge never runs, no EventSource opens, and live
updates silently stop.

Run installBridge immediately when document.readyState is already
past "loading"; otherwise keep the DOMContentLoaded handler for the
fresh-load case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 09:11:46 +02:00
cmd/chat-posta Revert "auth: drop WebAuthn PRF; encrypt tokens with deployment-static key" 2026-05-15 19:10:23 +02:00
docs/adr docs: import CONTEXT.md and ADRs 0001-0004 2026-05-17 08:30:34 +02:00
internal internal/web/static: start live.js bridge even if DOMContentLoaded fired 2026-05-17 09:11:46 +02:00
CONTEXT.md docs: import CONTEXT.md and ADRs 0001-0004 2026-05-17 08:30:34 +02:00
go.mod internal/web: pure-function live bridge + scroll preservation contract 2026-05-17 00:02:08 +02:00
go.sum internal/web: pure-function live bridge + scroll preservation contract 2026-05-17 00:02:08 +02:00
PRD.md Revert "auth: drop WebAuthn PRF; encrypt tokens with deployment-static key" 2026-05-15 19:10:23 +02:00
README.md README: document chat.posta.no deploy procedure + cap_net_bind_service 2026-05-15 23:00:01 +02:00

chat-posta

The browser client for the posta federated chat ecosystem. chat-posta is a single Go binary that runs at chat.posta.no behind Caddy and acts as a multi-tenant Backend-For-Frontend over each user's personal posta-server. The browser speaks HTMX-flavoured HTML to chat.posta.no; chat.posta.no holds the user's session and proxies the posta-server REST + SSE API on their behalf.

See PRD.md (mirrored from the project tracker) for the full specification. Issues and slice tickets live on the GitHub-equivalent tracker.

Build

go build ./cmd/chat-posta

Static, cgo-free build for the Alpine deployment:

CGO_ENABLED=0 go build -ldflags="-X main.version=$(git describe --tags --dirty)" \
    -o chat-posta ./cmd/chat-posta

Run

chat-posta serve --listen :8080 --db /tmp/chat.db

chat-posta version prints the linker-stamped version.

Config

Layered, precedence flag > env > config > default:

Flag Env TOML key Default
--listen CHAT_POSTA_LISTEN listen 0.0.0.0:80
--db CHAT_POSTA_DB db /var/lib/chat-posta/state.db
--config /etc/chat-posta/config.toml

Layout

cmd/chat-posta/        cobra root, `serve`, `version`
internal/web/          HTTP handlers, html/template, embed.FS assets
internal/config/       TOML + env + flag layering
internal/store/        SQLite open, migration framework, schema
internal/auth/         WebAuthn + AES-GCM (stubbed; slices 3-4)
internal/posta/        typed client for posta-server (stubbed; slice 3+)
internal/sse/          per-user SSE multiplexer (stubbed; slice 8)
internal/admin/        invite/user CRUD for cobra subcommands (slice 2)

Templates and CSS/fonts are embedded via embed.FS so the binary is self-contained.

Deploy

Production chat.posta.no runs as a non-root user inside an incus container on fismen. The service listens on :80, which a non-root user cannot bind without the cap_net_bind_service file capability on the binary.

cp and cp -a do not preserve file capabilities — they are an xattr the kernel does not include in -a. Likewise, incus file push lands a fresh file with no caps. Every binary swap must therefore re-apply the capability before restarting the service. Without it, the service comes up and immediately exits with listen tcp :80: bind: permission denied.

# Build static binary (see Build above).
scp ./chat-posta fismen:/tmp/chat-posta-new

# Stage inside the container, atomically swap, RE-APPLY THE CAPABILITY,
# then restart. The mv is atomic on the same filesystem; the kernel
# keeps the old inode mapped for the still-running process.
ssh fismen incus file push /tmp/chat-posta-new \
    chat-posta/usr/local/bin/chat-posta.new \
    --mode=0755 --uid=1000 --gid=1000
ssh fismen incus exec chat-posta -- \
    mv /usr/local/bin/chat-posta.new /usr/local/bin/chat-posta
ssh fismen incus exec chat-posta -- \
    setcap cap_net_bind_service=+ep /usr/local/bin/chat-posta
ssh fismen incus exec chat-posta -- rc-service chat-posta restart

# Verify.
ssh fismen incus exec chat-posta -- rc-service chat-posta status
curl -fsS https://chat.posta.no/healthz

Do not rely on cp -a to make a rollback copy of the running binary — the copy will lack the capability and will not bind :80. If a rollback is needed, re-apply setcap to the restored file before restarting.