Case study · 01 / 05 · 202613 min read
  • Onboarding
  • Flutter
  • Edge cases

Couchsurfing Tours

How do you show members where the features went, without a tutorial they will skip?

Where
Couchsurfing. The Explore surface of the mobile app, and the desktop site mirrored from it.
Role
Sole designer, and the author of the mobile implementation: the Braze proof of concept, the design, 49 commits of production Flutter, the tracking plan. An engineer revised the architecture in review.
A phone held in two hands, the Couchsurfing Explore screen up: the Continue Exploring row with the Profile tour card, a photo of the member with friends on a tram, and the two buttons, Go to Profile and Take the Tour
49 commits, four daysNo result to claim

At a glance

  1. The problem

    After the redesign, members who had used the app for years thought features were gone when they had been moved or renamed. Support tickets said so, and so did my own monitor reading Reddit. The ask from product was a way to teach features. The first idea was more upsell-style cards.

  2. What I did

    Designed a spotlight tour system, proved it in Braze, found that Braze cannot reach a widget inside a Flutter app, and wrote it natively myself: a carousel of tour cards on Explore, an overlay that dims the screen and cuts a ring around the real control, and a coach mark that follows the member into whatever they open. Four days, 49 commits, one pull request.

  3. What happened

    It merged under my name after an engineer revised the architecture to close the mobile lead's review, and it is live in the app. I left before it could be measured, so there is no result on this page, only what shipped and what the device and the review taught me.

01Scope

  • 49

    commits in four days, all mine

  • 59

    files across seven packages of the production app

  • 5,500

    lines of Flutter, give or take, and 13 test files

02The problem

The redesign moved things. Members who had used Couchsurfing for years opened the new app and could not find their trips, the filters that find the right host, or events, which used to be called something else. Support tickets said it. My own feedback monitor, SEN-MO (opens in a new tab), the bot that reads Reddit and the app stores every morning, said it more often than anything else about the redesign. The pattern was the same each time: a feature was assumed missing when it had been moved or renamed.

Some of the complaints were right. Before my time, a few features had been cut on purpose, and no tour brings those back. The real brief was narrower than the loud one. Show the features that still exist, and be ready to show new ones as they ship.

My VP of Product put it as a need to help new users learn our features, and the first thought on the table was more cards in the upsell style the app already used. I took the ask and spun it a different way. Mobile games walk you through a level before they let you play. I wanted that, with one change for people like me who skip every tutorial: a second button on every card that takes you straight to the feature instead of walking you there. It shipped on iOS. The Android build and the desktop site show one button.

The case against a tour is a fair one, and I made it to myself first. A tour patches an information-architecture problem instead of fixing it. That is true, and it is also true that the redesign was not going to be redone, that a tour ships in days, and that a card carousel on Explore is a place to add every feature we release after this one.

03What I dropped

Braze first, then everything the device argued me out of.

  • Delivering the tours through Braze, so copy and targeting could change without a releaseBraze cannot see a Flutter widget.

    I built it there first: a tour anchored to page elements in plain HTML and CSS, to prove it needed no developer time. It worked on the web. Then Flutter, which paints its own widgets, and Braze cannot read them. The mechanism has to be native, and once it ships in a release a Braze payload only buys copy edits, at the price of a broken tour being a campaign nobody can debug from inside the app. That finding is why I ended up writing the code myself.

  • Parking a lost tour instead of ending it, with a bar offering Resume or DismissA restart is cheaper than a resume.

    I built it, ran it and reverted it the same day. Resuming from wherever a member wandered off to means navigating back from an unbounded set of places. A restart from the card is cheaper on tours this short, and the better fix was to stop accidental exits: a shake and a glow when a tap lands outside the target.

04The five tours

I chose the tours from the most repeated claims in the tickets and the monitor, which were also the core of the product.

  1. 01Profile. Where your profile lives now, what it looks like, and where to edit it. I forgot how to reach it myself more than once, and I did not design that screen.
  2. 02Hangout. Going live, and seeing who else is. The tour walks the go-live drawer: what you are up for, then a detail and where. It points at the switch without pressing it, because going live is a real state change and stays the member's call. Mobile only, since the feature does not exist on desktop.
  3. 03Trips. What a trip is and how to make one, because a trip has to exist before a stay request can be sent. The tour goes through the library to Your Trips, then to New Trip.
  4. 04Hosts. Where to find hosts, and where the filters live that turn a wall of results into people who can actually take you. The loudest complaint of all was the recently-active filter not being on by default, and members not knowing how to turn it on. The tour walks the three trust filters under one spotlight and ends on Apply.
  5. 05Events. A feature people thought was missing, under an old name. Members were seeing curated events instead of member events, so the tour points at the filters. This tour was born on desktop, where Hangouts does not exist, and was added to mobile after.

The cards carry the app's illustrated style, with one exception. The Profile card shows the member's own photo, because the tour is about them.

Three phone captures of the Continue Exploring row on the Android build: the Profile card with the member's own photo, the Hosts card marked Done, and the Events card marked Done.
The row on the Android build: Profile with the member's own photo, Hosts and Events marked Done. iOS carries a second, direct button on each card.

05The lock on the Hosts card

The gate

The Hosts tour ends inside host search, which sits behind a minimum-complete profile. On a fresh account the tour walked the member straight past that gate, pushed host search over the Add to Profile sheet the app had just opened, and left that sheet stranded underneath. I found it on a test account whose profile was barely started.

The card says so first

It frosts, the two actions become one Complete Profile button into the sheet the app would have opened anyway, and the count in the copy comes from the same source the sheet reads, so the ask cannot change between the card and the next screen. Locked cards sort behind the ones you can act on and ahead of the ones you have finished, because a locked tour is still work worth doing and a done one is history.

Two guards

The lock checks the loading state as well as the completeness flag, because the flag reads true while the checklist is still loading on a cold start. Without that the card rendered unlocked and then dropped a lock onto it, and it had been invisible in testing because the first run after launch sails through. And the tap re-checks before it starts anything, so a tap that lands in the gap between launch and the checklist's answer cannot start a tour the card would refuse a moment later.

On the phone

This is the card on a test account three details short of the minimum. It says what it needs before the tap, and the button opens the same sheet the app would have opened anyway.

06What the device taught me

The spotlight worked on the first day. On the second, on a real phone against real layouts, three of the four tours then on mobile died. Each ended cleanly through the timeout I had built for a target that never appears, which was the safety net earning its place on its first outing, and each death had the same cause: the target existed in the widget tree and was not on screen. Fourteen discoveries follow, in the order I hit them, one card each. My commit subjects are the headings.

01

Point every tour at something actually on screen.

Every tour launches from a row you have scrolled down to, so the chips above it are off screen by then. The Hosts tour now opens on host search. A trips step aimed at a discovery module a new account may never get was dropped.

02

Aim at a control.

A key on a list container never resolves to a spotlightable rectangle, even with the list plainly visible. The rule I wrote down for whoever adds the next tour: target discrete controls.

03

Tours follow the user in instead of teleporting them.

Tapping the spotlighted widget advanced the step but never navigated. Only the coach mark's Next did. So the tour raced ahead to wherever Next would have gone while the member went wherever the button took them, then sat on the wrong screen until the timeout killed it. It had only ever worked on the Profile tour, where the real button happens to lead exactly where the next step lives. Now a step can wait for the real tap, and the tour goes wherever that tap goes: into the filter sheet, into the go-live drawer.

04

Retry the reveal until there is something to scroll to.

A target inside a sheet still animating open was looked for once, missed, and never looked for again. Only an attempt that actually scrolled counts as an attempt.

05

Centre a revealed target instead of nudging it into view.

The least possible scroll leaves the target flush against the bottom edge with the coach mark squeezed into whatever is left. Landing it mid-viewport shows the thing instead of barely revealing it.

06

Decide wrong screen by time, not by whether the target is mounted.

A pushed route keeps the screen underneath alive, so the trips card was mounted and findable while completely covered. Mounted is not showing. About a second of failing to resolve is the honest signal, and then the tour navigates to the screen the step names, once.

07

Correct the screen on the tap.

Landing on the wrong page and being moved a few seconds later feels broken even when it ends up right. The tap-through now navigates itself, one frame after the real control's own navigation lands, so the member sees one move instead of two.

08

Only the real element advances the tour.

The ring is the target inflated for looks, and a tap on the ring counted as a press: the tour advanced, the app did not, and the next step went looking for a screen nobody had opened. A wide tap dismissed the tour. Interaction now tracks the real bounds. Only the drawing stays generous.

09

Decline taps on look-only steps.

A search field is worth pointing at and must not be pressed: the keyboard covers the explanation and shifts the page under the spotlight, and the results walk off into a screen the tour cannot follow. Those steps catch the tap and answer with the same shake and glow an outside tap gets.

10

Place the coach mark by which side has room, and cap its height.

The card measured room to the bottom of the screen, which counted the gesture bar as space, so on the last step of one tour the Done button sat under the bar. Then a fixed threshold picked the wrong side on a short phone. Now the side with more usable room wins, insets are respected on both ends, and a long step scrolls its copy inside a height budget while the buttons stay pinned. At larger text sizes the whole card used to scroll and the only way out was the system back button.

11

Catch the system back button where the app actually dispatches it.

The obvious listener never saw the press. The app hooks the platform channel lower down and pops the route first, and that pop was the break this had to fix. Registered at the same level, only while a tour is up. Back now steps the tour back and stays on the step's screen, and on the first step it leaves the tour without closing the app.

12

Back out of a sheet, and stop step-skipping.

A sheet is a route pushed over a screen, not a screen, so going back from the trust filters stepped the tour while the sheet was still open. Steps now say whether they live in a sheet. And two quick taps on Next advanced twice, because the coach mark kept showing the old step through its transition. One move per step.

13

Do not time out a tour that is still travelling.

Two reported failures were the same clock: a step whose target lives on a screen that has to load burned the whole budget waiting for a screen that was on its way. The budget is for a target that will never resolve, not for a slow network, so it re-arms every time the app lands somewhere new. Time only counts against a tour that is sitting still.

14

Animate finished cards to the end, and re-sort between launches rather than under the user.

A card that just finished announces itself, then files itself away. Ordering reads a baseline taken at launch, so a row someone is already looking at never reshuffles under their thumb.

01 / 14

07The design before the code

I drew in code first. The Braze proof of concept was the sketch: anchoring came before anything else, then the coach mark, then the transitions. The Figma file came after the mechanism worked, and it holds every card state, including the locked Hosts card and a Hangout card for a member who is already live. Those frames stay in the file. The shipped screens are the design.

What changed once it ran on a real device is the section above. The design assumed a target was either there or not. The phone knew four ways a target could be missing: off screen, inside something still animating open, mounted under another route, or on a screen that had not loaded yet. Each got its own rule, and each is a card above.

The thing I fought longest was animation. Flutter in debug mode renders motion roughly, so the transitions looked worse on my phone than they would ever look for a member, and I had to judge smoothness through that. Once the peek-then-point transition looked right in debug I knew it would look better than right in a release build.

I wrote the implementation with an AI pair in the loop for testing and debugging on a real phone. Every decision on this page was mine, made against the device, and the review that follows treated the result as engineering.

08The review, and the second pair of hands

Before I opened the pull request I had a bot read the repository's recent pull requests, the ones that passed and the ones that failed, and write down why they failed. Most of it was mechanical: an analyzer that fails on a warning, generated files that must be regenerated and formatted, a coverage floor on new code, a custom lint package with rules about casts and records and where a bloc may be provided. I ran every check locally before anyone else saw the branch, and the last commits on the branch are that sweep.

The pull request drew sixteen inline comments and a changes-requested from the mobile lead. None of them were about taste. They were about where things live. Decisions belonged in the bloc and not in a widget. A layer that cannot see the screen should not hold user-facing copy. Value types belonged in the domain package with the other enums. I understood what was being asked. I did not have the time to go back and do it myself, and the engineers were short of work that week, so I asked a frontend engineer to make the revision pass. It merged under my name with his architecture changes in it. That is how it happened, and I would rather say it plainly than round it up.

It turned out to be the better call for a reason I did not plan. With the structure handled, that engineer spent his time on edge cases, and found ones I had not: what happens when a step's real control asks for location permission, a Braze modal colliding with host search mid-tour, and a member who is already live in a hangout when the Hangout tour starts. The Events tour today walks straight into the location prompt and comes out the other side.

Brandon had designed a multi-step user's tour through our large app, then he took the initiative to implement it within our codebase! It was really impressive how fast he was able to pick up a foreign codebase and build out a non-trivial task of focusing elements and stepping through the app.
The mobile lead, in writing, when I asked him for a line for this page

09Instrumentation

The tracking plan is the closest thing this page has to a results section, so here are the questions it asks. Every card tap reports the card and the slot it was in, because finished cards sink to the end of the row between launches and a first-slot tap is not comparable to a last-slot one. Every entry and exit of a tour reports through one listener rather than from each button, so Back, Skip, Next, tapping the spotlighted control and finishing the last step all land in the same funnel. A skip carries the step it was standing on. Declined outside taps report on their own: the tour carries on, but a step collecting those is a step whose coach mark is not landing. A lost target reports too, because a wrong target and a member wandering off look identical from inside the tour and only the volume tells them apart. Abandoning the app mid-step fires nothing, so that is started minus finished and skipped in the analysis.

I overtracked on purpose. I would rather have data I do not need than wish I had kept it. The two things I most wanted to know were whether more people would press the direct button than take the tour, which I expected, and whether the people who chose the tour skewed older, which I suspected. The first can only be read on iOS now: that build carries both buttons, and the Android build and the desktop site carry one.

10What shipped

The desktop site's Explore Couchsurfing Tours row: the Trips, Hosts and Events cards, with Hosts and Events marked Done, and the row's two arrows.
Desktop, built by the web team from the mobile design. Same cards, same copy. No Hangout, because the feature does not exist there.

11The tours, running

The five tours, recorded from the live app on an iPhone in September 2026. Step through them with the arrows, or a swipe on a phone. On a desktop the tour in view plays on its own. On a phone, tap the one you want.

01 / 05 · four steps

Profile

Three ask for the real tap. One is look-only with Next. The last tap opens Edit Profile and ends the tour there. On iOS the card carries both buttons: Go to Profile, and Take the Tour.

  1. 01Your LibraryEverything of yours lives behind here: your profile, your settings, all of it.
  2. 02Your ProfileTap your profile icon any time to view and edit your profile.
  3. 03Your Home DetailsYour place, as guests see it. Update it any time with Edit Home.
  4. 04Keep It Up to DatePhotos, story, home: the page behind this button is yours to shape. Tap it and make it feel like you.
01 / 05

12The impact

It is live. Open the app, scroll Explore to Continue Exploring, and the five tours are there, dimming the screen and cutting a ring around the real control. It merged a few weeks after the pull request opened and reached members after my last day. I saw it in production for the first time on 11 September 2026, through an emulator, while writing this page.

The review is the other thing I took from it. A mobile lead reading the pull request as engineering, and not as a designer's experiment, produced sixteen structural comments. The revision that closed them is on the page above, with the engineer's edge cases beside mine.

Nothing measured

There is no number here because none exists for me to withhold. The tracking plan is in place and the funnel was reporting to the analytics stack when I left, and I left before anyone could read it. I will not guess at a completion rate, and I will not borrow a benchmark. The source belongs to the company and stays out. The architecture is described in prose.

The design, the shipped screens, the edge cases, the tracking plan, and what I would read first.

Corrections & caveats

Cohort
Live in the app since August 2026. Five tours on Explore, four on desktop.
Confidence
Shipped and running. Nothing measured by me.
Not measured
  • The direct-to-feature button ships on iOS and not on the Android build or the desktop site I captured, so the tour-versus-direct comparison the tracking was built for holds on one platform.
  • Desktop was built by the web team from my design. I wrote the mobile implementation only.
  • The pull request merged after an engineer's revision pass closed the mobile lead's review. The architecture in the app is his revision of my structure.

13What I would measure next

  • Completion by tourfinished over startedFive steps against three. If Hosts loses people, brevity was right and the two cut steps stay cut.
  • The step people leave onskip and abandon, by stepA skip carries its step. The step that collects them is the one whose coach mark is not landing, or whose ask is too big.
  • Card taps by slotfirst slot to lastFinished cards sink to the end. Whether a card still gets tapped from the back of the row says whether the carousel is a row or a first card.
  • Lost targets per stepcount, with declined tapsA wrong target and a member wandering off look the same from inside the tour. Volume on one step is a bug. Volume everywhere is people.

14What surprised me

What surprised me was the room. I demoed it on a borrowed Android phone with the branch checked out, first to my VP of Product and then to the CEO when he got in, and got no notes back, which had never happened. Then the pull request went up, product approval was required, and the Slack thread filled with people reacting to a designer's name on a mobile pull request. The CTO said I was a mobile developer now and he would start adding me to the stand-ups. He was joking. The pull request merged anyway.