No description
Find a file
Niels Göttsch 964b0be064
All checks were successful
Build & Publish / Check (push) Successful in 2m25s
Build & Publish / Build & Publish (push) Successful in 5m33s
fix(docker): drop /var/run symlink from chisel rootfs for Kaniko
The runtime stage copies the chisel rootfs onto `/` in a scratch stage.
chisel ships /var/run as a symlink to /run; Kaniko replacing it forces a
RemoveAll of the destination /var/run, which on the Forgejo act runner
holds a busy /var/run/act bind-mount -> 'unlinkat ... device or resource
busy'. The image does not need /var/run, so remove the symlink.
2026-07-17 10:40:05 +02:00
.cargo chore: Switch to mold linker and fix Dockerfile build 2026-05-22 12:03:03 +01:00
.github ci: ignore busy /var/run/act mount during Kaniko scratch build 2026-07-17 10:17:52 +02:00
migrations feat: store bag finished_at as full datetime for correct timeline ordering 2026-02-24 21:38:39 +00:00
scripts fix: allow multiple origins for mixed roasts 2026-02-24 11:03:20 +00:00
src fix(cli): use Duration::from_mins for token wait timeout 2026-07-17 09:20:01 +02:00
static chore: Simplify formatting toolchain 2026-05-22 12:03:03 +01:00
templates feat(auth): generate additional invite links for onboarding users 2026-07-16 12:38:36 +02:00
tests feat(auth): generate additional invite links for onboarding users 2026-07-16 12:38:36 +02:00
.dockerignore build: add .dockerignore to reduce Docker build context 2026-05-22 14:59:56 +00:00
.gitignore chore: clean up stale Nix references 2026-05-22 14:59:39 +00:00
.mise.toml fix: support sqlx 0.9 2026-06-08 10:56:18 +00:00
build.rs fix(build): pass git hash to Nix builds via env var 2026-02-07 11:17:03 +00:00
Cargo.lock build(deps): bump sqlx from 0.8.6 to 0.9.0 2026-06-08 10:56:03 +00:00
Cargo.toml build(deps): bump sqlx from 0.8.6 to 0.9.0 2026-06-08 10:56:03 +00:00
CLAUDE.md chore: clean up stale Nix references 2026-05-22 14:59:39 +00:00
Dockerfile fix(docker): drop /var/run symlink from chisel rootfs for Kaniko 2026-07-17 10:40:05 +02:00
fly.toml chore: Switch to mold linker and fix Dockerfile build 2026-05-22 12:03:03 +01:00
LICENSE docs: add LICENSE 2026-02-03 20:53:45 +00:00
prek.toml chore: Simplify formatting toolchain 2026-05-22 12:03:03 +01:00
README.md Merge branch 'feat/additional-invite-links' into dev 2026-07-17 09:25:21 +02:00

cover image

B{rew}log is a self-hosted specialty coffee logging platform optimised for filter brewing enthusiasts. B{rew}log can be used for tracking roasters, roasts, brews, cafes and brewing gear.

B{rew}log features an LLM-powered "Bag Scanning" feature, which enables it to automatically fill roaster and coffee information using a photo of a bag. It also supports "check-ins" to log coffee enjoyed in a cafe.

B{rew}log ships as a single Rust binary that serves a web UI, a REST API, and a CLI client. The application uses SQLite as a backend, and will automatically create and migrate the database on start-up.

Note

This application was built almost entirely with Claude Code. I used this project as an excuse to explore the current state of the art in agentic coding tools. I've reviewed much of the code, and I'm largely responsible for the rules and the layout of the repository but nonetheless the vast majority of the code was written by Claude and Opus 4.6.

Quick Start (Demo)

Before you start, you'll need to sign up for Openrouter and Foursquare Places and get API keys for both.

Then create a docker.env file:

# You'll need an API key from OpenRouter
BREWLOG_OPENROUTER_API_KEY=sk-or-...
# I've had good results with Gemini models, but you can try 'openrouter/free' to experiment
BREWLOG_OPENROUTER_MODEL=google/gemini-3-flash-preview
# FourSquare Places API key for location searching
BREWLOG_FOURSQUARE_API_KEY=fsq3...

You can see the full list of configuration options below. Once your .env file is complete, start the container using the environment file

# Create a data directory to store the database
mkdir data
# Run the container
docker run \
  --rm \
  -p 3000 \
  --env-file docker.env \
  -v $PWD/data:/data \
  ghcr.io/jnsgruk/brewlog:latest

On first start with an empty database the server prints a one-time registration URL:

No users found. Register the first user at:
  http://localhost:3000/register/abc123...
This link expires in 1 hour.

Open that URL, choose a display name, and register a passkey. This creates an account and signs in automatically.

To onboard additional users later, generate a fresh invite link from the Admin page ("Invite" → "New Invite") or from the CLI (brewlog invite create). Each link is valid for 7 days and can be used once.

Install from Git

To build and install from source, you'll need a working Rust toolchain:

cargo install --locked --git https://github.com/jnsgruk/brewlog.git

Then create a .env file containing at least your OpenRouter and Foursquare API keys, and start the server:

brewlog serve

CLI Authentication

To use the CLI or API for write operations, create a token via browser hand-off:

brewlog token create --name "my-cli-token"
# Browser opens → authenticate with a passkey → token printed once

export BREWLOG_URL="http://localhost:3000"
export BREWLOG_TOKEN="<token from above>"

# Create data from the CLI
brewlog roaster add --name "Radical Roasters" --country "United Kingdom"

To invite another person, generate a single-use registration link (valid for 7 days):

brewlog invite create
# Prints a https://.../register/<token> link to share with the new user

Run brewlog --help for the full command reference.

Configuration

All settings are read from environment variables or CLI flags. A .env file in the working directory is loaded automatically via dotenvy.

Server (brewlog serve)

Variable Purpose Default
BREWLOG_RP_ID WebAuthn Relying Party ID (server domain) localhost
BREWLOG_RP_ORIGIN WebAuthn Relying Party origin (full URL) http://localhost:3000
BREWLOG_DATABASE_URL Database connection string sqlite://brewlog.db
BREWLOG_BIND_ADDRESS Server bind address 127.0.0.1:3000
BREWLOG_INSECURE_COOKIES Disable the Secure cookie flag (auto-enabled for localhost defaults) false
RUST_LOG Log level filter info
RUST_LOG_FORMAT Set to json for structured log output

CLI Client

Variable Purpose Default
BREWLOG_URL Server URL http://localhost:3000
BREWLOG_TOKEN API bearer token for write operations

Integrations

Variable Purpose Default
BREWLOG_OPENROUTER_API_KEY OpenRouter API key for AI extraction required
BREWLOG_OPENROUTER_MODEL LLM model for AI extraction openrouter/free
BREWLOG_FOURSQUARE_API_KEY Foursquare Places API key for nearby cafe search required

Development Setup

Install mise then set up the development environment:

sudo apt update
sudo apt install -y clang mold pkg-config libssl-dev \
    libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 \
    libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 \
    libcairo2 libasound2t64 libnss3 libxshmfence1
mise trust
mise install              # Install all dev tools
mise run install-e2e      # Install Chrome for Testing + ChromeDriver
uv tool install prek      # Install prek (git hook runner) as a standalone tool
prek install              # Install git hooks

Note: prek is a standalone binary installed via uv and must be on PATH. If it is missing, prek run -av cannot invoke cargo fmt -- --check and reports failed to run cargo fmt -- --check — this is the missing prek/toolchain, not a formatting error. Installing prek resolves it.

Contributing

prek run -av              # Run all lints, tests, formatters
mise run fmt              # Format all files
mise run check            # Full CI validation (fmt + lint + test)
mise run test             # Run all tests
cargo build               # Build

See CLAUDE.md for architecture, code patterns, and development conventions.

CI / build pipeline (this fork)

This fork lives on the zo Forgejo forge (git.ziemlichoptimal.de/uberbau/brewlog, default branch dev) and is deployed to the moby homelab via Flux GitOps. .github/workflows/deploy.yml runs on every push to dev: it lints/tests (check), then builds and publishes the container image to the forge's own registry as git.ziemlichoptimal.de/uberbau/brewlog:{dev,<sha>}. The moby deployment pins that image by digest and bumps it per release.

The build is daemonless — it does not use Docker. It runs Kaniko as the job container, building the Dockerfile (context pulled straight from git) and pushing to the registry. No docker, no buildx, no privileged Docker-in-Docker.

Gotchas (learned the hard way)

  • Kaniko does not support BuildKit RUN --mount=type=cache. The build fails on it. Keep every RUN in the Dockerfile plain — no BuildKit cache mounts. (This is why the Rust build layer is a plain cargo build; we have been bitten by this before, so do not "re-add caching" to the Dockerfile.)
  • The forge's Actions runner is node20-only. Pin JS actions to their node20 generation (actions/checkout@v4, actions/cache@v4, and if you reintroduce docker actions, @v3/@v6) — the newer @v5/@v6/@v7 releases declare runs.using: node24, which the runner rejects (must be one of [… node20 …]).
  • Reference actions by full github.com URL (uses: https://github.com/owner/repo@ref). The runner's default action mirror (code.forgejo.org) is incomplete/flaky and aborts clones mid-run.
  • The check job installs only Rust via mise (install_args: rust); the full mise.toml toolchain pulls tools from the GitHub API, which 401s on the runner.

License

Apache License 2.0