I found a new home at Goatmire
Goatmire reminded me why unfinished experiments and the people around them matter as much as shipping software.
- Published
- Reading time
- 8 min read
I went to Goatmire expecting a Nerves and Elixir conference. I came home feeling that I had found another community I wanted to belong to.
The timing was good. Coding agents and large language models have made my normal programming work slightly mad. An agent can read a repository, change several parts of it, and run the test suite before I have finished thinking through the first change. I love what this makes possible. I also notice how easily a day becomes a chain of tasks completed at machine speed.
At Goatmire, I spent part of a workshop making a native desktop counter. Elsewhere, I turned a crow sample into a musical instrument. Neither needed a customer or a roadmap. I had forgotten how much I missed that kind of programming.
A reassuringly large part of the Elixir Berlin user group had made the trip too. I got to spend time with familiar people away from our usual meetup and met many bright people I had not known before. People listened to unfinished ideas. Conversations picked up again in the next hallway or over the next drink without anyone having to call it networking.
I do not mean “home” in a grand sense. Goatmire felt like another place where I could bring something half built, admit what I did not know, and expect curiosity rather than posturing. That is rare enough to notice.
Programming without a ticket
Most of my code starts with a reason. A client needs something, a service is too slow, or a tool is missing. Even my side projects tend to acquire issue trackers and release plans before they deserve them.
At Goatmire, I let curiosity choose the next step. I was not trying to finish a client task. I wanted to see a BEAM application open its own desktop window. Then I wanted to know whether a crow could carry a melody.
This was not a rejection of agentic programming. I am still excited by it. Goatmire simply reminded me that faster completion leaves more time to explore only if I actually use some of that time for exploration.
A native Elixir window
The workshop that stuck with me was Damir Batinović’s introduction to Emerge and Solve. Emerge draws native interfaces from Elixir. Solve handles state without tying it to a particular interface.
My workshop project was a counter, about as glamorous as counters usually are. Its state fit in one controller:
defmodule Example.Counter do
use Solve.Controller, events: [:increment, :decrement]
def init(_params, _deps), do: %{count: 0}
def increment(_payload, state),
do: %{state | count: state.count + 1}
def decrement(_payload, state),
do: %{state | count: state.count - 1}
end
The viewport looked up that controller and connected its events to buttons:
credits = solve(Example.App, :credits)
Input.button(
[Event.on_press(event(credits, :increment))],
text("+")
)
Solve updates the controller, then the viewport asks Emerge to render again. The counter and viewport run under a normal OTP supervisor. The application code has no browser tab and no JavaScript bridge.
The dependencies are equally plain:
defp deps do
[
{:emerge, "~> 0.4"},
{:solve, "~> 0.3"}
]
end
Before this workshop, I associated an Elixir interface with Phoenix, LiveView, or a terminal. Now a native window is on that list.
Back home, the project also caught one bit of workshop sloppiness. It requires Elixir ~> 1.20, but my default shell runs 1.19.5, so mix test refuses to start. I had not committed a .tool-versions or mise.toml file. The code survived the trip; its toolchain did not. Next time I will record both.
The Murder
The music workshop went in a less sensible direction. My Goatmire folder now contains several Sonic Pi tracks, a lot of medieval techno, and a crow instrument called The Murder.
Sonic Pi ships with a sample named :misc_crow. I cut a tiny piece from the caw and sent it through an echo with a very short delay. The reciprocal of a note’s frequency gives the delay needed for a tuned comb filter:
define :murder_phase do |n|
rt(1.0 / midi_to_hz(note(n)))
end
define :caw do |n, opts = {}|
with_fx :echo,
phase: murder_phase(n),
decay: rt(opts.fetch(:decay, 1.5)),
max_phase: rt(0.06) do
sample :misc_crow,
start: 0.1,
finish: 0.16,
amp: opts.fetch(:amp, 1) * 0.3
end
end
The crow provides a noisy attack. The short echo provides the pitch. Once that worked, I kept going. Rookery Techno uses crows for its kick, snare, hi-hat, bass, and arpeggio. Carol of the Crows turns the familiar bell ostinato into a round for plucked birds.
It was completely unnecessary. I learned how the delay time in a comb filter determines pitch, and I had fun writing it. I had missed writing code that only needed to do that.
KMS, finally on Hex
I also brought a serious project. On the lightning track, I presented KMS and published its first package on Hex.
KMS began inside another Elixir application before becoming its own OTP application. I wrote about why I separated it. It can run inside an application with SQLite, use PostgreSQL, or expose an HTTP API when the key authority needs a separate deployment.
The smallest example creates a key-encryption-key alias and encrypts a value with additional authenticated data:
{:ok, _kek} = KMS.create_alias("owner:alice")
{:ok, encrypted} =
KMS.encrypt_alias(
"secret",
"owner:alice",
aad: "document:123/body"
)
{:ok, "secret"} =
KMS.decrypt_alias(
encrypted,
"owner:alice",
aad: "document:123/body"
)
The aad value is not secret. It binds the ciphertext to that document and field. If someone copies the ciphertext to another field, decryption with the new context fails.
The package installs as a normal Mix dependency:
defp deps do
[
{:kms, "~> 0.1.0"}
]
end
I had already spent enough time wondering whether KMS needed another pass before release. Once I had explained it to a room full of people, keeping it off Hex felt silly. I published it.
Git hosting from Europe
One of the people I met was Peter Ullrich, who is building Pushin.eu. I wholeheartedly support his attempt to build a sovereign Git service in Europe.
Pushin says it keeps repositories in the European Union under European jurisdiction. It supports ordinary Git over SSH and HTTPS, and its API follows GitHub’s request and response shapes closely enough for many existing tools. The pun import command runs on the developer’s machine, so the GitHub token stays there.
What convinced me was the boring exit path. A normal git clone still gives me the complete repository history. Issues and pull requests are available through the API. Moving to a European host should not require handing control to a different kind of trap.
What I brought home
The most honest record of Goatmire is still sitting in ~/Projects/Goatmire/. It contains an unfinished counter that currently refuses to compile in my default shell and more crow techno than anyone requested. Both make me want to keep programming.
I am not going to use fewer agents. I am going to be more careful about what I use the time they save for. Not every experiment needs to become a product, and not every useful conference result fits into a work plan.
I found a new home because the people at Goatmire made curiosity feel normal. I want to keep exploring this field with them, including the corners that look ridiculous at first.
KMS made it onto Hex because I finally stopped preparing to release it. If encryption belongs in your Elixir application’s data model, try the package, read the documentation, or inspect the source. If the API or documentation gets in your way, open an issue and show me where.