Skip to content
IndexCase 02
Expedition operations

Cairn

Back-office software for a mountaineering expedition company, where three roles share one operational universe and the same URL renders three different products.

Role
Self-directed
Timeline
19 days
Stack
TS · Next.js · No UI library
Team
Solo
cairn-expedition.vercel.appmain · seeded
Overview
Live operations
Worlds
3
Modules
14
Capabilities
11
Expeditions
10
Fig. 01 — Cairn, live operations2026
Chapter 01

The problem

Roles sharing one universe
3
Modules they diverge across
14
Route files serving them
1

Authorisation is an architecture problem long before it is a UI problem.

A director runs the operation, a field lead commands one expedition, and a participant checks in from the mountain. They need the same data and almost none of the same screens — and the moment that distinction lives in components, every new screen inherits the question of who is allowed to see it.

Expedition management was chosen because it demands every hard class of the problem at once: strict role scoping, time-sensitive coordination, live tracking, incident management and archiving. Most enterprise software touches one or two of those. This touches all of them.

Chapter 02

Architecture

Three worlds — Plan, Operate, Record — as a type, not a navigation pattern. Fourteen modules behind a single dynamic route, gated server-side before anything renders. Authorisation is a capability list rather than a role check, so components ask what they may do and never who is asking. The universe is one deterministic seed, relational all the way down.

Layer 01
Worlds

Plan, Operate and Record are a string literal union, not a navigation pattern. Adding a world is a one-line change TypeScript validates end to end.

Layer 02
Capabilities

Eleven capabilities are the unit of authorisation. Components ask can("operations:command") and never which role is asking, so roles can be redefined without touching a screen.

Layer 03
One route

Fourteen module workspaces behind a single dynamic route, with the capability check running server-side and redirecting before anything renders.

Layer 04
Seeded universe

One seed string produces the same ten expeditions, people, incidents and gear every build — relational by ID, so a participant appears in the roster, the manifest and the logbook as the same person.

// src/app/(shell)/[world]/[module]/page.tsxif (mod.requiredCapability) {  const role = (await readDemoRole()) ?? "director";  const caps = ROLE_CAPABILITIES[role];  if (!caps.includes(mod.requiredCapability)) {    redirect(`/${world.key}/${fallback?.key}`);  }}
Chapter 03

The interface

The Live module is the argument: the same expedition at the same URL is an operator workspace for a director and a check-in panel for a participant. Not a hidden button — a different product.

Live operations
Interaction notes
The role switch is the demo

Switching from director to participant on the Live module does not hide controls — it renders a different workspace against the same expedition.

Gated tabs disappear

A module a role cannot use is absent from the tab bar rather than disabled, and typing its URL redirects to the first module that role can reach.

Check-ins propagate

A participant's check-in writes to a subscribed store, so the lead's expedition feed updates without a request or a refresh.

Fig. 02 — Live operations2026
Chapter 04

Performance

Five dependencies and no client-side data fetching produce the numbers you would expect: Lighthouse scores 100 on desktop and 98 on mobile against the live deployment, and the main thread never registers a blocked frame. The interesting figure is the one nobody reports — a first request to an idle deployment paints in 5.8s rather than 0.5s, because the platform is still waking up. That cost is real and it belongs to the first visitor of the day.

First paint — cold start5.8s
First paint — warm0.5s
Lighthouse — cold start56
Lighthouse — warm100
Outcome
14
Modules

Across three worlds, served by one dynamic route

11
Capabilities

The whole authorisation surface, as a string union TypeScript checks

5
Dependencies

next, react, react-dom, clsx, cva — everything else is hand-written

Next case — 03
Chrono
  • Rigorous authorisation, no authentication. The server guard trusts a role cookie the browser sets.
  • Eleven capabilities and exactly one of them writes. Everything else is a read.
  • The participant's check-in reaches the leader's feed instantly. Both are the same browser — there is no second user.
  • The README says no inline styles. There are eight, each a percentage no class can express.