clubPal Exists Because Nobody Could Remember Whose Turn It Was

We have a munchie club. Once a month we go out to eat, and each of us takes a turn picking the restaurant. It's one of my favorite things we do, and for a long time it came with exactly one recurring problem: nobody could remember whose turn it was.

It sounds like the smallest problem in the world, and it is, right up until the week you need the answer. Someone skipped a month because they were traveling. Two people swapped. Someone new joined. And now the group chat is scrolling back through half a year of messages, trying to reconstruct an order that only ever existed in everyone's slightly different memory of it. Then, once you finally know whose turn it is, there's the rest of it: picking a date, chasing people to say whether they're coming, booking a table for the right number, and working out who owes what at the end.

None of that is hard. It's just mental load, and it lands on whoever happens to care most that month. In 2024 we started building something to carry it instead. It's the oldest idea in palStack, older than the finance app or the pantry app, and for most of its life it was called minglePal. In June 2026 it became clubPal, which is a better description of what it actually is: a place for a club to keep track of itself.

We're in our second year of picking restaurants this way now, and it's been amazing.

The whole first version was one question

The first job was to answer "whose turn is it?" and to remember the answer, so none of us had to.

That turns out to be a small amount of code. Every club has a list of members and a pointer to whose turn it currently is. Moving to the next person is just stepping one place along the list, and wrapping back to the start when you fall off the end. This is the real function, trimmed down a little:

def advance_rotation(group_id):
    # Members in their custom order if the club set one, otherwise by join date
    members = members_in_rotation_order(group_id)
    current = current_turn_index(group_id)

    if rotation_type == "round_robin":
        next_index = (current + 1) % len(members)
    elif rotation_type == "random":
        next_index = random.randint(0, len(members) - 1)
    else:  # "none": the turn stays put until someone moves it
        next_index = current

    save_turn(group_id, members[next_index], next_index)

The % len(members) is the whole trick. If there are six of you and it's the sixth person's turn, the next index wraps around to the first person again. The group chat was trying to do that arithmetic in its head, across months, with people joining and leaving. A computer does it without ever getting it wrong.

Fair is harder than it sounds

What surprised me was how much thought one question needed once it had to work for more than our own club.

The obvious answer is round robin: everyone in a fixed order, forever. But that assumes the order itself is fair, and the first order is usually just whoever joined first. So clubs can set their own order, and the app only falls back to join date for anyone the club hasn't placed yet.

Not every group wants an order at all. For them there's random, where the next person is drawn each time. It can feel less fair in the moment, since the same person can come up twice, but it suits a group that likes the surprise. And some groups don't rotate at all. They have one person who always organizes, and they just want the rest of the app. That's the "none" option, which does nothing, on purpose.

Three ways to answer "whose turn is it," and each one is somebody's correct answer. That was an early lesson in building for groups: there usually isn't one right behavior, there's the behavior this particular group has already agreed on, and the software's job is to remember it.

What a month looks like now

Once the app knew whose turn it was, the rest of the mental load started peeling off one piece at a time, and it's easiest to explain as a month in our club.

Whoever's turn it is picks the restaurant and creates the event, and the app already knows they're the host, because it's their turn. Everyone gets asked whether they're coming. Before the RSVP deadline, the people who haven't answered get a nudge, which means the host no longer has to be the one doing the nudging. When the deadline passes, the host gets a final headcount, which is the one number you actually need to book a table. If the group can't agree on something, there's a poll. At the end of the night, someone photographs the receipt and the app splits the bill, so nobody's doing division on a napkin. Afterwards, people can rate the pick, and the ratings add up to a small leaderboard of who chose best.

None of that was in the original plan. Each piece showed up because the step before it worked, and that made the next small annoyance easier to see.

The bug that taught me the most

The clubPal lesson I think about most is from this September, and it wasn't about rotation at all.

Our demo server had a networking problem after some containers restarted, and the part of clubPal that lists your clubs started failing. The app handled that failure by showing what it shows when you have no clubs yet: a friendly empty screen and "0 Clubs." Nothing looked broken. Nobody reported it. It went unnoticed for days, and we only found it when an automated check went looking for problems on purpose.

For a social app, that's the worst possible way to fail. "You have no clubs" isn't a neutral message. It's the app telling you your friends aren't there. An error screen would have been annoying. An empty one was quietly wrong about something people care about. The lesson I took from it applies to everything we build: an error and an empty state should never look the same.

Why this one matters to me

Of all the pals, clubPal is the one that's least about managing stuff. finPal is money, pantryPal is groceries, propertyPal is a house. clubPal is about people showing up for each other once a month, and the software's only real job is to take the organizing off their hands so that nobody has to be the one who carries it.

That's what it's done for us. Two years in, the only thing our club has to think about is where to eat.

← Previous post Next post →