← ~/blog

Building circl.pl - and what vibecoding is actually like

The idea

Every group of friends has the same broken loop: someone drops "drinks Friday?" in the chat, three people react with a thumbs-up, two ask "where?", nobody actually commits, and by Thursday the plan has scrolled into oblivion. The group chat is where plans go to die.

circl.pl is my attempt to fix that. It's a friends-hangout coordinator: you post where you're going, everyone sees who's in, you vote on the plan, and you sort out who brings what - all in one place the whole circle can see, instead of scattered across a chat nobody can search.

It's a web-first PWA (installable to your home screen), so there's no app store, no download, just a link. You sign in with Apple or Google and you're in.

The stack

If you've read my tradegame.org post, this will look familiar, because I deliberately reuse the same shape for everything I self-host:

  • Next.js frontend, exported as static files
  • Go + Gin API, layered as handler to service to repository
  • PostgreSQL 16 for the data
  • Caddy in front for TLS and reverse proxy
  • The whole thing as one docker-compose.yml, deployed on a small VPS behind the same Caddy that already runs tradegame

Boring, on purpose. When the plumbing is identical across projects, I stop thinking about plumbing and spend the attention on the actual product. A deploy is one command: pull, rebuild the service that changed, health-check.

Now the interesting part: vibecoding

I built most of circl by vibecoding - working alongside an AI coding agent, describing what I want, reviewing what it writes, and shipping in a tight loop. Not "type a prompt and pray," but more like pairing with a very fast junior who has read the entire codebase and never gets tired.

Here's what that actually feels like in practice.

The loop is absurdly short. A friend messaged me: "as an admin I can't leave a circle someone else made - there's no button." Twenty minutes later there was a Leave button, a confirm dialog, a test locking the permission rules, a green CI run, and it was live on circl.pl. Same session, another friend: "the notification bell overflows off the screen on my phone." Fixed and shipped in the same batch. The gap between feedback and deployed fix collapses to minutes, and that changes how you build - you stop hoarding changes into big releases and just ship the next small thing.

You still have to make the decisions. The agent is fast, but it doesn't know that "crew-only privacy" is a lie until you tell it to think about it. Which brings me to my favourite story from this build.

The crews that had to die

Early on I added crews: sub-groups inside a circle, like "the girls" or "the climbing crew," with an option to make an event visible only to that crew. It felt clever. It shipped.

Then I actually looked at the access control, and it was fake. Joining a crew only checked that you were in the parent circle - so any member could self-join any crew, read its "private" events, and leave again. The lock was decorative. The door just asked "are you in the building?" and let you in.

The honest fix wasn't to bolt an approval step onto crews. It was to delete them entirely. circl already had a real privacy primitive - a private circle, with genuine membership control - and crews were a confusing, insecure duplicate of it. So they went: the feature, the database tables, the UI, all of it. Net minus 900 lines in one pull request.

The lesson generalises: a privacy boundary is only as strong as its admission check. If anyone can enter the set a "private" flag scopes to, the flag is a label, not a lock. Before you trust any visibility toggle, trace who can get into the thing it's scoping.

The vibecoding angle here matters too. Deleting a whole feature cleanly - unwinding it from the model, the migrations, the API, the frontend, the tests - is exactly the kind of tedious, wide-blast-radius change that you'd normally put off. With an agent it's an afternoon. Cheap deletion is underrated. It keeps the codebase honest.

The guardrails that make it safe

Vibecoding without a safety net is how you ship confident nonsense. The net for circl is a set of integration tests that boot the real stack - API, database, migrations - and assert the whole access-control matrix end to end: who can see which circles, who can delete what, who can leave, that an unauthenticated request gets bounced. They run on every push and pull request via GitHub Actions, and they gate the deploy. If they go red, nothing ships.

That's the deal I've made with the speed. The agent can move as fast as it likes inside a fence that says: prove the important invariants still hold, on a real running system, before this reaches anyone. When I nuked crews, the same suite is what told me I hadn't quietly broken event visibility for everyone else.

An honest take

What vibecoding is genuinely good at: boilerplate, wiring, refactors with a big blast radius, writing the tests you'd otherwise skip, and holding the whole codebase in its head so you don't have to. It removes the friction that usually stops small improvements from happening.

What it is not good at: knowing what's worth building, spotting that your clever feature is a security hole, or deciding when to delete something. That's still the job. The agent writes the code; you own the product and the taste. The crews story only had a happy ending because a human went "wait, that privacy doesn't actually work."

The short version: vibecoding didn't replace the thinking. It deleted almost everything around the thinking, which turns out to be most of the work.

Try it

circl is live at circl.pl. Start a circle, drop it in your group chat, and post the next plan there instead of watching it drown in messages. It's free, and I'm actively building it based on what my own friends complain about - so if something annoys you, that's exactly the feedback that ships a fix by the end of the week.