Skip to main content

AI Agents Love Gleam

· 12 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

Fair warning: this post contains some opinions that are going to be controversial and may not age well. Here be dragons.

AI coding agents like Claude Code, OpenAI Codex, and Google Gemini can write code, run it, read the errors, and try again. That loop is the whole game. The faster and more informative that loop is, the more useful the agent becomes. After building Curling IO Version 3 in Gleam alongside AI coding agents, I'm convinced Gleam is the best language for this workflow. Agents don't write better Gleam - there's less training data. But Gleam's compiler lets agents self-correct without waiting for a human.

Parallel Tests for Free

· 5 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

While writing the previous post about our per-test SQLite databases, I was describing how each test gets its own in-memory database, no shared connections, no shared state. And I thought: wait, if nothing is shared, can we just run them all at the same time?

Turns out we could, and our server test suite went from ~4 seconds to ~0.85 seconds for around 800 tests. Zero code changes to the tests themselves. One 25-line Erlang module.

Test Isolation for Free with SQLite

· 9 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

Most web frameworks treat test database isolation as a hard problem. Rails has database_cleaner with three strategies. Django wraps every test in a transaction it rolls back. Phoenix does the same with its SQL sandbox. They all exist because tests share a single database server, and that shared state is the root of flaky tests and ordering dependencies, the kind where a test passes alone but fails in the suite.

Curling IO Version 3 doesn't have this problem. Each test gets its own database. Not a transaction. Not a truncated copy. A completely independent in-memory SQLite database, cloned from a template in microseconds using SQLite's backup API.

Why We Chose SQLite

· 11 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

We assumed PostgreSQL for Version 3. After a decade running Postgres in production, why would we even consider something else? We knew the tooling, the failure modes, the operational playbook. Postgres is the safe choice for good reason.

Then we looked at what "self-hosting Postgres" actually involves, compared it to what Litestream does for SQLite, and changed our minds. This post covers the decision, the architecture, the trade-offs, and why we'd make the same call again.

Background Jobs Without the Baggage

· 6 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

In most web stacks, adding background jobs means adding infrastructure: Redis, Sidekiq, a separate worker process, a monitoring dashboard, another thing to deploy and keep running. Curling IO Version 2 uses Delayed Job backed by PostgreSQL, which works well but requires a separate worker daemon alongside the web process.

Curling IO Version 3 runs on the BEAM (Erlang's virtual machine), and background jobs are just another process in the same runtime. No Redis. No separate worker. No additional infrastructure. This post covers how we built it, why we chose SQLite persistence over in-memory queues, and how the whole thing fits into a few hundred lines of Gleam.

Passwordless Auth, Done Right

· 11 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

Curling IO has been passwordless since Version 2. No passwords to remember, no passwords to steal, no password reset flows. You enter your email, we send you a short-lived login code, and you're in. It's been working well for over a decade, and for Version 3 we're keeping the same approach while fixing some rough edges and adding multi-email support.

But first, let's talk about why we made this controversial decision in the first place.

Bilingual by Design

· 7 min read
Dave Rapin
Dave Rapin
Founder @ Curling IO

Curling IO serves hundreds of clubs across Canada, where English and French aren't optional, they're official languages. A club in Quebec needs a fully French experience. A national organization like Curling Canada needs both. Rails has mature i18n support and Version 2 has been fully bilingual from the start, but after a decade of maintaining around 10,000 YAML translation keys, we've hit the limits of what that approach can catch: missing keys, missing translations, and unused keys that accumulate silently over time.

In Version 3, we wanted compile-time guarantees that make those problems impossible. This post covers how we designed the i18n system, why we split it into two layers, and what we changed from Version 2.