Case study

Planary

A platform my classmates and I built because school projects don't teach you to actually ship.

Year2024 – ongoing
RoleFull-stack developer
Team3 classmates
Duration6 months to first launch
Planary — main screenshot
4
sub-apps shipped
3
person team
6mo
idea to live

Planary started as an honest answer to a frustration: school projects let you coast. You write code, demo it once, and nobody is accountable for what happens next. We wanted to know what real shipping feels like — where users notice when it breaks. The answer was to build a platform, put it on a real domain, and use it ourselves.

School builds don't hold you accountable.

You can fake progress in a school project. Miss a sprint, catch up the night before the demo, collect the grade. Nothing actually has to run tomorrow — so most of it doesn't. We kept finishing projects that were technically complete but practically useless, software that existed only long enough to be assessed.

We wanted to break that pattern. The rule: everything goes on a real domain and real users — us — rely on it. If the auth service goes down, nobody logs in. That kind of pressure doesn't exist in a classroom. So we manufactured it.

One auth layer. Four real products.

Rather than a single app, we decided to build a platform: a shared identity layer that any sub-app could plug into, then stack real tools on top. Each sub-app had to solve a problem we actually had, so we'd be honest users ourselves and notice when something broke or annoyed us.

We chose Go for the backend — none of us had used it seriously before. That was intentional. Learning a new language under real delivery pressure is very different from learning it in tutorials, and we wanted the hard version.

Four sub-apps, one sign-on.

Each sub-app is independently deployed but shares the same auth context. Log in once at auth.planary.ch and your session carries across all apps automatically.

Auth
Single sign-on service across all sub-apps. JWT with refresh tokens, cross-subdomain cookie propagation, and a centralised user store. Any new sub-app gets authentication for free.
Dashboard
The hub after login. Aggregates recent activity, links to all sub-apps, shows quick stats. The front door of the platform.
Tournamount
Tournament bracket manager. Create brackets, seed participants, record results. Matches update in real-time via WebSockets — built for and used in our class gaming sessions.
Wishlist
Collaborative wishlist with real-time sync. Multiple users, shared lists, live updates. Built to stop us sending Amazon links over Discord.
Tournamount — live bracket updates via WebSockets
Tournamount — live bracket updates via WebSockets
Wishlist — real-time collaborative lists
Wishlist — real-time collaborative lists

Why these tools.

Every major decision was made deliberately, usually after trying the obvious thing first and finding it wanting.

Go on the backend
Wanted explicit error handling and compile-time guarantees after fighting JavaScript's permissiveness. Go's goroutine model turned out to be a natural fit for WebSocket fan-out — each connection gets a goroutine, channels handle broadcast.
Redis pub/sub
Real-time updates go through Redis pub/sub channels keyed by resource ID. WebSocket handlers subscribe on connection and unsubscribe on close. Clean separation between transport and storage.
PostgreSQL
Tournament brackets are relational by nature — rounds, matches, participants, outcomes with foreign key chains. Tried to model this with Redis alone first. Gave up quickly. Postgres was the right call from the start.
Docker Compose
Local dev must match production exactly. We've had zero works-on-my-machine incidents since we standardised on Compose for every service from day one.

The honest retrospective.

  • Distributed auth has more edge cases than you expect. Concurrent refresh token requests, cross-subdomain cookie propagation, and logging out everywhere are each simple in isolation — together they multiply.
  • Go's concurrency model changed how I think about concurrent code. Goroutines are cheap; the mental model of ownership via channels is cleaner and more explicit than I expected from a systems language.
  • Team velocity is not linear. Three people communicating poorly ship slower than one person. We got much better at short async decisions and not blocking on full consensus for small choices.
  • Ship the ugly version. The embarrassing build we had at week two was the most useful thing we made. Actually using it ourselves shaped everything that came after it — far more than any planning session did.
← Back to workMilla Homes case study →