← Back

Barranco

An adventure into building a real-time canyoning planner using mostly Claude Code.

demo.barranco.app ↗
Barranco landing page showing the tagline 'Plan the trip. Skip the chaos.' over a canyon background

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

The frontend and backend talk through two paths. Requests go down via a typed RPC client generated by AshTypescript (an Ash extension, as the prefix might suggest), so React calls Ash actions directly. Updates come back up through typed Phoenix Channels: Ash resources declare publish rules, and AshTypescript generates typed channel definitions so the frontend gets compile-time checking for real-time events too.

Tech stack

Backend

Elixir · Ash Framework · Phoenix 1.8 · Phoenix Channels · Oban · PostGIS · PostgreSQL

Frontend

React 19 · TypeScript · Mantine · TanStack Query · Leaflet · Framer Motion

Infrastructure

Hetzner · Kamal · Buildkite · PostHog · Sentry · Resend

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.

Trip schedule showing ongoing, tomorrow, and later sections with canyon names, times, and participant counts
The trip schedule updates live as participants join or leave.

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.

Weather forecast widget showing temperature ranges, precipitation, and rain probability for each day of the meetup
Weekly forecast for the accommodation location, with daily temperature and precipitation.

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.

Interactive topographic map with canyon markers alongside a sortable list of nearby canyons with difficulty ratings and drive times
Nearby canyons shown on a topographic map, sorted by drive time from the camping.

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.