Dominik Süß

fighting computers since 1999

in 

December Adventure 2025

Code something for the joy of it


The goal of December Adventure is to code a bit every day and write about it. Using this as an opportunity to fill my blog with some content while also holding myself accountable to actually code a bit every day.

This year, I want to build something with gleam! It has been on my radar for a while, and I've decided it's now time to finally commit to writing something useful in it.

I decided to work on a tool to simplify money management for our sports club parkour.wien. We're a pretty small community and don't have any membership fees or recurring costs but once a year, we rent a school gym to train indoors over the cold season. As we're an officially registered Verein, we have to disclose our finances at the end of each year and manually tracking attendance and season passes gets tiring pretty fast.

This year we tried out OpenCollective. It works well enough for our use case but leaves a lot to be desired, as it's mostly targeted at groups who don't have their own bank account. Additionally it doesn't support multiple accounts which we'd love to also keep track of cash contributions in person.

Day 1

Setting up the base project and dependencies. For now, this is just a simple wisp web server plus some SQL based code generation. Since I'll be dealing with money, I wanted to try out pgledger to avoid running something larger like tigerbeetle. I've managed to set up migrations and will tackle SQL generation next to make sure I can actually talk to the database and utilize the pgledger API.

Day 2

I had to replace TIMESTAMPTZ in pgledger with TIMESTAMP but this got squirrel to work! For now, I've got a basic HTTP server that serves a list of accounts. No way to create them yet but this will probably come tomorrow. I'll still have to get used to the templating approach of writing HTML in gleam but for now this is useable.

Day 3

Today I wanted to work on a wireframe to get a feel for the initial user experience. I tried some ideas but wasn't happy with any of them. So I'll try and build out the functionality first and see how things might fit together 🤷.

Day 4

On this misty day, I tried my hand at testing. Gleam projects come with gleeunit by default so all it took for me was to find a way to mock DB responses. My mind was still in go land at this point so I wanted to reach for my trusty interface to be able to inject a mock in tests and the real db when running the application. Turns out, gleam doesn't have interfaces (or traits for what it's worth). This threw me off but after reading this wonderful article by Kayla, I found a solution I'm quite happy with!

I've created a module repo.gleam which contains a type alias for the operation I want to perform:

pub type GetTransactions =
  fn(String) -> Result(List(Transaction), QueryError)

This is now what my handler accepts:

pub fn transactions(req: Request, fetch: GetTransactions) -> Response {
  todo
}

To actually get the data from the database, I created a function in pgrepo.gleam that returns a GetTransactions closure when passed a database connection:

pub fn get_transactions(db: pog.Connection) -> GetTransactions {
  fn(name: String) -> Result(List(Transaction), QueryError) {
    let result = sql.list_entries_for_account(db, name)
    case result {
      Ok(pog.Returned(_rows_count, rows)) -> todo
      Error(smth) -> todo
    }
  }
}

With this in place, I plug in the new function in the router like this:

pub fn handle_request(req: Request, ctx: Context) -> Response {
  use req <- web.middleware(req)

  case wisp.path_segments(req) {
    ["transactions"] -> transactions(req, pgrepo.get_transactions(ctx.db))
  }
}

And I can replace the function with whatever I want in tests:

pub fn transactions_shows_empty_state_when_no_transaction_test() {
  let response =
    router.transactions(
      simulate.browser_request(http.Get, "/transactions"),
      fn(name: String) -> Result(List(Transaction), QueryError) {
        assert name == "collective"
        Ok([])
      },
    )
  assert response.status == 200
  assert simulate.read_body(response)
    |> string.contains("No transactions found")
}

Day 5

This week was also a Hackathon at Grafana Labs so I had to constantly switch between hackathon and "production grade" mindset. Now that I know the different abstractions that make sense for gleam web applications, I cleaned up the codebase and split things up into multiple modules.

As one of the advantages of functional programming is easier testing, I also want to take this opportunity to do more test driven development (where applicable). I want to avoid spinning up a database for each test so I'll try to keep functions as pure as possible and rely mostly upon unit tests. Once the interface is somewhat useable, I'll also introduce e2e tests with a real database.

Day 6

I started on a setup wizard yesterday and finished it up today. This gave me a chance to play around with input handling & validation. It turns out, this was a great introduction to the use expression. It allowed me to validate whether any accounts exist and if so, disable the setup wizard.

pub fn handle(req: Request, list_accounts: ListAccounts) -> Response {
  use <- forbid_when_accounts_present(list_accounts)
  todo
}

This way, I can chain validations, input parsing and other prerequisites without having a bunch of indentation levels.

I also experimented with form validation but I'm not completely satisfied yet. For now, I'm happy that this works how I want it to. This setup wizard may not stick around but implementing this definetly made gleam click for me.

Day 7 & 8

Did a lot of sports on Sunday and we remodeled our garden on Monday so nothing to report here.

Day 9

I started on implementing OIDC/Oauth2 handlers to build out the authentication flow. I'll keep this as abstract as possible so it could be published as wisp_oidc or similar. Also learned about ETS which I'll use to temporarily store session information/auth state using carpenter.

Day 10

My workday was scattered with meetings so I didn't really get to implement a lot of things (tried out carpenter but no progress on the OIDC front). However, the talks from Lambda Days 2025 got uploaded to youtube and they contained a lot of usefull gleam information. I especially enjoyed these talks:

Especially the talk about testing really resonated with me and I'm looking forward to trying fable for this project.

Day 11

On this beautiful day, I won the fight with dex and finally have a working test identiy provider. Turns out the container entrypoint just silently absorbed any arguments and I was starting the service without my config file 🤦. Did some more groundwork for OIDC but nothing to write about.

Day 12

No time today, had to run 18km for our marathon training.

Day 13

I spent my morning finishing up OIDC support. For now, this isn't generic enough to be published but we'll see how it looks like when cleaned up. It's a bit of a mess right now and I'll need to clean this up quite a bit before I'm happy. I also found an upstream issue in the ywt package for which I submitted a merge request. Let's see if I get a response on that one 🤞.