Measured before choosing: 86.17%, identical via `mix test --cover` and via the exported sample + `mix test.coverage`, stable across seeds. Floor is the integer below that, with the rule written next to it: a FLOOR, ratcheted up as behavior-test yaks land, never lowered (a drop means the change or an exclusion is wrong). Exclusions are the modules the suite cannot or should not exercise, each justified in the config comment: `use`-generated glue (Repo, Mailer, Telemetry, ErrorHTML/ErrorJSON), the `use FirehoseWeb` macro, production release tasks (Release), the lib_dev credo check (NoConnShadowing), and test scaffolding (DataCase, ConnCase, AccountsFixtures, Test.FakeBlog). Everything else counts, CoreComponents/controllers/LiveViews included. Wiring matches blogex and the fast/push split: `test.coverage` runs the suite once with `--cover --export-coverage default` (deleting stale exports first, since `mix test.coverage` imports every .coverdata it finds and would pool them), then the `coverage` sensor enforces the floor from that export. The sensor guards on the export existing because `mix test.coverage` exits 0 when it finds nothing — a floor check that did not run must not pass.
Firehose
Willem's personal blog — a modular static blog built on Blogex, powered by Phoenix and NimblePublisher.
Posts live as markdown files in git and compile into the BEAM at build time: zero database, zero runtime I/O, sub-millisecond reads.
The project structure is modular, I wanted to have a personal blog, and an easy way to add an 'engineering' blog to fledgling SaaS products, as well as release notes. The release notes now also serve to announce new features on my blog.
Status: Experimental. Works on my server ;-).
Enjoy, Willem van den Ende
Project structure
This is a monorepo with two parts:
firehose/
├── app/ # Phoenix application (OTP app: :firehose)
│ ├── lib/firehose/ # Application logic
│ ├── lib/firehose_web/ # Web layer (controllers, live views, components)
│ ├── lib/firehose/blogs/ # Blog definitions (engineering, release notes)
│ ├── priv/blog/ # Markdown posts
│ └── mix.exs
├── blogex/ # Blogex library (multi-blog engine)
│ ├── lib/
│ └── mix.exs
├── Dockerfile # Deploy build; ARG pins must match mise.toml
├── mise.toml # Runtime versions (Elixir, Erlang, Node)
└── scripts/check-pins.sh # Verifies the pin sync; runs in `make check`
Two blogs are configured:
| Blog | Route | Description |
|---|---|---|
| Engineering | /blog/engineering |
Main blog |
| Release Notes | /blog/releases |
What's new in an app |
Prerequisites
- Elixir 1.15+ and Erlang/OTP 26+ (managed by mise)
- Node.js (managed by mise, for Tailwind + esbuild assets)
- PostgreSQL 16+ running on
localhost:5432
Setup
1. Install runtimes
mise install
This installs Elixir, Erlang, and Node at the versions specified in mise.toml.
Toolchain pins
mise.toml (local dev toolchain) and the Dockerfile ARG pins (deploy
builder image) must agree — local builds and deployed images should run the
same Elixir/OTP. make check runs scripts/check-pins.sh to verify this;
when it reports a MISMATCH, fix the file it names. Upstream drift of these
pins (newer Elixir/OTP/Debian releases) is reported separately by
dokku-updates firehose check from the dokku-maintenance repo.
2. Start PostgreSQL
You need PostgreSQL running locally with a postgres user (password: postgres).
Option A — Docker (quick):
docker run -d --name firehose-pg -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:16
Option B — Dev container:
cd .devcontainer && docker compose up -d db
Option C — System install:
Install PostgreSQL normally and ensure a postgres user with password postgres exists.
3. Install dependencies and set up the database
cd app
mix setup
This runs:
mix deps.get— fetch Hex and git dependenciesmix ecto.create— create thefirehose_devdatabasemix ecto.migrate— run database migrationsmix run priv/repo/seeds.exs— seed initial datamix assets.setup— install Tailwind and esbuildmix assets.build— compile CSS and JS assets
4. Start the server
mix phx.server
Visit localhost:8056 in your browser.
Note: The dev server runs on port
8056by default. Override withPORT=4000 mix phx.server.
Development commands
All commands below are run from the app/ directory.
| Command | Description |
|---|---|
mix phx.server |
Start the dev server with live reload |
iex -S mix |
Start an IEx session with the app |
mix test |
Run the test suite |
mix precommit |
Run all quality gates (compile, credo, format, test) |
mix format |
Format Elixir/HEEx code |
mix credo --strict |
Run Credo static analysis |
Writing blog posts
Posts are markdown files with Elixir frontmatter. Place them under app/priv/blog/<blog-name>/:
app/priv/blog/
├── engineering/
│ └── 2026/
│ └── 05-01-hello-world.md
└── release-notes/
└── 2026/
└── 05-01-v0-1.md
Frontmatter format:
%{
title: "Hello World",
author: "Willem van den Ende",
tags: ~w(elixir phoenix blogex),
description: "My first blog post"
}
---
This is my first post. It compiles into the BEAM at build time.
In dev mode, edited .md files trigger live reload automatically.
Blogex
The Blogex library is a multi-blog engine for Phoenix apps, powered by NimblePublisher. See blogex/README.md for full documentation.
License
MIT — see LICENSE