Inspiration
A year ago I watched a few people climb alone in a Copenhagen bouldering gym which I thought was a shame because climbing is such a social sport. However finding someone to climb with meant posting in one of several Facebook groups and hoping someone would answer. There were no filters, no alerts, and no single place to look. People who are new in town, new to the sport or whose friends are booked for weeks had it hardest.
What it does
ClimberBase helps climbers find others to climb with. You just post when and where, others join, you chat together, then meetup at the gym. Starting in Copenhagen.
- One feed of open sessions across Copenhagen's gyms: gym, day and time, type, level, the faces of who's going and the spots left. Guests can browse before signing up.
- Filters and alerts: gym, activity (bouldering, top rope, lead), level, group size and a same-gender preference. The bell turns your filters into a push alert for new matching sessions, no account needed.
- Join in one tap, then add it to your calendar. Upcoming and past sessions live in the Plans tab.
- A group chat per session to sort out the details before meeting.
- Host your own session in seconds: gym picker, day, time slot, style, level, group size and a note.
- Profiles with stats and climbing history.
- Safety and moderation: block and report, phone verification, and host tools to remove, mute or hide messages.
- Share links that open the session in the app or fall back to the store, a post-session check-in, and a feedback board where users post and vote on ideas.
- Danish and English.
How I built it
- Kotlin Multiplatform with Compose Multiplatform for the whole UI. About 93% of the mobile
client is shared Kotlin: 60k lines in
commonMainagainst 1.4k Android-only and 2.7k iOS-only lines, plus 253 lines of Swift in the iOS shell. - All 11 feature modules are 100%
commonMain: feed, create session, session detail, chat, profile, settings, auth, check-in, feedback, sessions and donation. - Ktor backend on the JVM sharing the API contract (the
:coreDTO module) with the app. Supabase Postgres and Auth, Firebase Cloud Messaging for push, deployed on Railway. - Stack: Kotlin 2.3.21, Compose Multiplatform 1.11, JetBrains Navigation 3, Koin, Ktor client (OkHttp and Darwin engines), Coil 3, kotlinx-serialization and DataStore, Sentry KMP and PostHog.
- Platform code is small and sits behind
expect/actual: push tokens, keychain and Tink storage, SMS one-time-code autofill, the in-app review prompt, share sheet, calendar, maps and photo picker. - About 3,500 tests. ViewModels, repositories and Compose UI are tested in
commonTest. There are also Robolectric host tests, Kotlin/Native iOS tests, 81 Ktor end-to-end test files, XCUITests, baseline profiles, and k6 load tests at 100 concurrent users.
Challenges I ran into
- The cold start. A partner app with an empty feed is dead on arrival. I planned the launch around a two-week "seed window": nine friends host six sessions a week. Alerts work without an account, so an empty feed still leaves a reason to stay. Community stats stay hidden until they are worth showing.
- Real people arrive, including the wrong ones. In the soft-launch week, spam sign-ups from outside Denmark started disturbing real climbers. I delayed the public launch by a week and, in four days, shipped a server-driven kill switch, device identity, Danish phone verification and host moderation tools (versions 1.1.0 and 1.1.1).
- Both stores at once, solo, after work. One codebase helped a lot. Being solo meant I had to make all the decisions which was quite burdensome, especially around design. I iterated a lot with AI on different designs until I landed on the ones I'm proud to show. Doing this after work has been a test of endurance especially with balancing real life with it.
- Lag on iOS. I was surprised to find that some animations stuttered the first time they ran on iOS. It turned out to be Metal pipeline caching that Compose Multiplatform doesn't fully optimize yet. To fix it, I warm up the app's animations behind the first landing screen, and subtly throughout the app, so the caches are filled before anyone sees a stutter.
Accomplishments that I'm proud of
- Overall design and UX. Especially the gym selector. That thing took me too many iterations to settle on!
- Finishing what I started. My main motivation was to learn the skill of staying consistent and focused across a long stretch of time.
- Landing a partnership with a local gym who's partnering with me on digital and physical promotions.
- Building my own product that is an expression of my own craft and what I value in software experiences.
- Wearing all kinds of hats: product, design, engineering, QA, manager, marketing, sales.
What I learned
- It takes way more effort than I thought to launch a polished small scope product. You gotta be ruthless with cutting scope for v1.
- Distribution is really half the job!
- I developed a builder mindset. I'm restless if I'm not making something in my free time.
- Productive AI workflows: managing parallel work streams effectively, how to speed up development while keeping quality high, internal tooling.
- Got more familiar with iOS (being an Android person)
- Social apps are tough to nail. Lots of safety requirements. How to reach the critical mass necessary and kick it off. I don't think I wanna build a social app for my next project.
What's next for ClimberBase
- Ad campaign starting now for 2 weeks. I hope I get Copenhagen fully engaged with the app.
- Supporting more cities in Denmark and the world, following a localized marketing campaign for each. A user doesn't care how many users you have in the world, they care how many they can climb with in their city.
- Implement more features from the backlog: private sessions and invites, climbing trips, coaching services, used equipment marketplace, etc.
Built With
- coil
- compose-multiplatform
- firebase-cloud-messaging
- koin
- kotlin
- kotlin-multiplatform
- ktor
- postgresql
- posthog
- purchases-kmp
- railway
- remotion
- resend
- revenuecat
- sentry
- supabase
- twilio
Log in or sign up for Devpost to join the conversation.