![cover image](./static/og-image.png) **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](https://openrouter.ai) and [Foursquare Places](https://foursquare.com/products/places/) and get API keys for both. Then create a `docker.env` file: ```env # 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](#configuration). Once your `.env` file is complete, start the container using the environment file ```bash # 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: ```bash 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: ```bash brewlog serve ``` ### CLI Authentication To use the CLI or API for write operations, create a token via browser hand-off: ```bash 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="" # 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): ```bash brewlog invite create # Prints a https://.../register/ 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](https://crates.io/crates/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](https://openrouter.ai/) API key for AI extraction | **required** | | `BREWLOG_OPENROUTER_MODEL` | LLM model for AI extraction | `openrouter/free` | | `BREWLOG_FOURSQUARE_API_KEY` | [Foursquare](https://foursquare.com/) Places API key for nearby cafe search | **required** | ## Development Setup Install [mise](https://mise.jdx.dev/) then set up the development environment: ```bash 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`](https://docs.astral.sh/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 ```bash 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](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,}`. 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](https://github.com/GoogleContainerTools/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](LICENSE)