Barranco
An adventure into building a real-time canyoning planner using mostly Claude Code.
demo.barranco.app ↗
Canyoning clubs organize treffens: multi-day gatherings at a base location (e.g. a camping) with daily trips to different canyons. Coordination usually happens through WhatsApp groups or in-person. It kinda works, but info ends up scattered and in people's heads. Additionally, there's important info such as weather and water levels that you need to be aware of, and that usually entails consulting various models and aggregating the output in your head. I wanted one tool that handles the logistics and pulls and aggregates important info automatically.
The backend runs on Ash Framework, a declarative Elixir framework where you define a resource once and it derives migrations, queries, authorization, and a typed API from that single definition. I thought it'd be a good match with Claude given its declarative nature.
Architecture
Tech stack
Backend
Frontend
Infrastructure
Overview
Ash Framework: "model your domain, derive the rest"
Ash is a declarative framework for Elixir. You define a resource once: its attributes, actions, relationships, and authorization policies. From that single declaration, Ash derives database migrations, query interfaces, and validation logic. On top of that, AshTypescript generates a fully typed TypeScript RPC client, so the React frontend calls generated functions that map 1:1 to Ash actions. Adding a field means changing one file. The migration, API, types, and policies all update automatically.
Authorization is policy-based and declared on the resource too. For example, only trip participants can claim equipment, and only club admins can approve join requests. These rules live next to the data model, not scattered across controllers.
Real-time updates
When someone joins a trip, claims equipment, or posts a comment, every connected client sees the change immediately. No polling, no manual refresh.
This used to be a bunch of hand-written notifier modules that manually
built payloads and called Phoenix.PubSub.broadcast. Now
each Ash resource declares a pub_sub block with publish rules
— which actions to broadcast, to which topics, with optional filters and transforms.
Ash handles the rest. On the channel side,
AshTypescript.TypedChannel generates typed channel definitions:
the frontend gets compile-time type checking for every event name and payload
shape, so a renamed event breaks the build instead of silently dropping updates.
On the frontend, TanStack Query listens for typed channel events and selectively invalidates only the affected queries, so the UI refetches just what changed.
Multi-model weather forecasting
(Mountain) weather is unreliable from a single source. Barranco fetches forecasts from 6 different weather models via Open-Meteo. All 6 models are fetched in parallel and cached for an hour, with a per-location cooldown to avoid hammering the API. The UI displays all models so canyoneers can compare predictions and make informed decisions. Canyoneers are typically pretty hands-on, so showing all models side-by-side works better than trying to blend them into a single forecast.
Water levels
Water is often the deciding factor for determining whether a canyon is safe to descend. Barranco fetches gauge readings from French and Swiss water authorities in parallel and finds the nearest stations by Haversine distance. Better would be to only show stations of rivers that run through the canyon, since that might absolutely not be the case if you're only going off on distance, but that's a lot more difficult so I deferred it.
Canyon database
Canyon data comes from OpenCanyon, an open database maintained by the Austrian Canyoning Association with around 6800 canyons across 41 countries (CC BY-NC-SA 4.0). A bulk import pulls the full GeoJSON dataset — coordinates, difficulty ratings, waypoints for parking and canyon start/end points. A separate enrichment worker then visits individual canyon pages for specs that aren't in the API: number of rappels, max rope length, total descent, whether a shuttle is needed, and a photo for the thumbnail.
Shuttle planning
Most canyons start and end at different points. The usual approach: drive all cars to the exit, leave dry clothes, then get into as few cars as possible and drive to the entry. After the descent, the shuttle drivers go back to the entry and pick up cars that were left there.
The algorithm picks shuttle cars greedily: sort drivers by capacity, select the biggest cars until there are enough seats, and leave the rest at the exit for retrieval. If one shuttle run isn't enough, it calculates multiple passes.
It's a small bin packing problem, and a greedy solution doesn't cover every edge case perfectly, but groups are small enough that it works well in practice. And it's simple, which is an advantage on its own.
Landing page
I wanted something bold and cinematic, given the somewhat extreme nature of canyoning. There's not a lot of source material, though. Most canyoning vids are low-quality, taken with a GoPro or something similar. There's a couple of really-high end movies on YouTube by brave souls that took their DSLR into the canyon, which I maybe could have used, but seeing as Replit Animations coincidentally launched at the same time, I ended up giving that a go. It's prompt-driven 'motion graphics' generation, as they put it. Initially I envisioned a person rappelling down the rope with the camera wall-facing, but the model was having a lot of trouble generating realistic rope movement. Ended up cutting the human out of the animations and just going with a movie in nature. The result is a beautiful (and entirely artificial) movie of a river flowing through a canyon. Really happy with the results. It doesn't loop well, but that's an issue for another day.
Deployment
Kamal deploys Docker containers to a €3 Hetzner VPS with zero-downtime rolling deploys. CI runs on Buildkite with custom runners on a different Hetzner VPS. CX33 seems to handle 3 runners max, which is the cap of the free Buildkite tier anyway.