We're hiring a CTO! Learn more

Scaling Tuple's Service to Millions

Since Tuple launched, there have been 4 different services used to track user presence and securely exchange messages between clients to start calls1.

Each transition has had its own pain points, and I don’t say that lightly: swapping out network stacks in a native app while maintaining protocol compatibility is a real vibe.

Let’s start by taking a quick look at the different solutions we’ve used over the years to get a better idea of the problems we’ve had to solve as we’ve grown, and how we ended up rolling our own blog-worthy service.

From Action Cable to Ably

Before I joined, there were questionable decisions made when it came to scaling, but that’s super fair: the main goal up until that point was to get the product out and validate it.

Tuple launched using Action Cable to simplify operational overhead and keep everything running as a single service. Once the COVID hit, though, it quickly became a dumpster fire.

The service was running on Heroku with all of its Dynos maxed out. Despite this, it had to be monitored around the clock and restarted every few hours. Sprinkle a little pandemic in the mix, and you had the perfect recipe for a very bad time.

There were really only two choices to be made: continue maintaining it and figure out a way to scale, or find a provider which would do the heavy lifting.

It was ultimately decided to migrate clients over to Ably, which is a fully managed service where you pay based on usage. Ably provides several SDKs, including one in Objective-C which could be used natively.

There were issues related to Ably’s client stability, but we were able to commit fixes and have them merged upstream after I joined. I’m kind of a dinosaur on the Mac, so I’m pretty comfortable with Objective-C.

Regardless, migrating to Ably was a huge win: we certainly never lost any sleep over service outages and could focus on building extremely important user-facing features, like sending confetti celebrations to your pair.

Getting ready for Linux

Fast forward a year or two later, and we found ourselves wanting to target platforms other than the Mac. Linux, by popular demand, was at the top of the list.

Tuple’s engine relies on WebRTC, which we use directly rather than through a browser (or Electron). This lets us reduce overhead significantly and tweak codecs to deliver high-resolution screen shares with crisp text. WebRTC is written in C++, and up until this point we’ve chosen to tread very lightly around its code base to avoid complexity.

On the Mac side of things, we could interface with WebRTC through its Objective-C adapters which play nicely with Swift. The adapters let us maintain our protocol-specific code in Swift, iterate quickly with less boilerplate code, and keep the C++ demon in its cage. This wouldn’t really suit us moving forward, though: Swift isn’t a language we’d like to bet on using across different platforms.

The writing was on the wall: we were going to have to double down on C++ development. Time to enter the thunderdome!

Firebase to the rescue

If we were going to invest more in our C++ codebase, we’d need to figure out how to bring Ably along. The only problem was, Ably doesn’t provide a C/C++ client, probably because they aren’t masochists.

As you can imagine, most service providers don’t provide bindings to lower level languages, let alone provide support for Linux. We were happy to find one exception: Google’s Firebase.

Firebase is a collection of services that can be used as building blocks to deliver real-time features, like user presence2. It’s popular with game developers, so they have an actively maintained C++ SDK. Score one for Tuple. Our only apprehension was that Google has a history of killing off its products, but we felt fairly confident that Firebase might stand the test of time.

Firebase was going to be our knight in shining armor. Or so we thought…

Side-loading Firebase

After a few weeks, we had a drop-in replacement for our Ably integration ready. The founders still had recurring nightmares about the migration from Action Cable, so we decided to try something clever. We weren’t jumping from a burning ship so we had time on our side to experiment.

Our plan was to ship our Firebase integration alongside our existing Ably one so that both services could talk to each other. We built in a remote switch that we could use to roll specific users or teams to Firebase, all while maintaining compatibility with users who were still using Ably. This would let us switch users incrementally and keep an eye on our support queue. If our experiment started to fail, then we’d just switch everyone back to using Ably without needing to deploy client updates.3

Over the course of a few days, it became clear that Firebase did not work as advertised: some people would appear online indefinitely if their clients disconnected unexpectedly. This wouldn’t be a show stopper if it was a bug within the client, as it’s open source, but the problem appeared to originate remotely.

We spoke with Firebase support, opened up issues, and even were able to provide reproducible test cases, but we were told repeatedly that things were working as expected. It wasn’t until a year later that they acknowledged / fixed the bug.4

After taking a little time to lick our wounds and accept the fact that no one would save us, we knew we really only had two options: write our own Ably client, or write our own service.

It seemed like it was a good time to invest in ourselves.

Our solution: The Realness

Prior to joining Tuple, I took a few years off from developing platform-specific code and wrote real-time microservices in Go. I had a high degree of confidence that we could create our own service and make it scale. It was our destiny ✨

This time around, we knew we had a market fit with a very active user base, and knew exactly what features we’d need in our own service. This would make it much less complex to deliver a solution without extra cruft.

I was able to write our service5 mostly over the span of a few weeks: it felt so good to be writing Go again and reasoning about concurrency more naturally. Although I took the lead, I’d Tuple with other members on the team every day so that the service became familiar to everyone.

We wrote loads of tests together to simplify the amount of time it would take to validate each requirement we had for the service. Each server is backed by Redis in a way such that data from an unhealthy instance would be automatically deleted. Each test case uses actors connected to different servers, backed by Redis server(s). This made it relatively straightforward to cover tricky scenarios like scaling up or down, nodes dying unexpectedly, or replaying missed messages with microsecond precision.

Rolling it out

Once our service was ready, we started placing bets on whether or not we could bring it to its knees. Matt, one of the other engineers at Tuple, worked on setting up a stress testing environment which he deployed to multiple regions across the globe. I guess you could say things were getting pretty serious.

With a fleet of virtual honey badgers ready to attack our service, we felt drunk with nerd power. Over the course of a few runs, we managed to get up to a few million concurrent connections while maintaining round-trip times within milliseconds. Honestly, we never found the service’s limit, but we figured a few million should be our baseline because it sounded cool.

Realness with millions of

With our stress tests running, we validated that our servers would auto-scale and distribute their workload across multiple zones on demand. At this point, we felt pretty good about trying the side-loading approach again.

We deployed our home-brewed solution alongside Ably using the same approach as before, but we didn’t need to do any rollbacks this time. While none of our user’s noticed, we knew we hit a major milestone and had a solid foundation to start porting Tuple’s engine to other platforms. Our engineering channel on Discord was lit — sometimes the best releases are ones where you can hear crickets chirp.

Realness reduces Tuple's binary

Closing thoughts

Today, our real-time service powers both our Linux and Mac apps using a small, portable C++ client. It went live over a year ago and it hasn’t required any code changes in about 10 months. We hardly have to think about its maintenance and it’s added zero stress to our workload.

Although we’ve switched our real-time infrastructure out a few times, we were able to move fast and not break things. We never felt like we were blocked from delivering new features while any of the transitions were happening behind the scenes. It really paid off to keep things simple until we were forced to solve problems which were no longer hypothetical.

I hope you enjoyed reading about part of our journey.

Until next time!

About the author

Hi! I’m Mikey, the first full-time engineer at Tuple. I was hired in the height of COVID and have helped Tuple scale while the world went remote. Since I’ve been on the team, my role has grown from being the Mac expert to a software architect, where I help shape our engine to grow alongside new hires, platforms and features.

You can find me on lurking on Twitter @YoungDynastyNet.

  1. See https://webrtc.org/getting-started/peer-connections for more information. 

  2. Their documentation uses presence as an example application. They also have blog about it. 

  3. This is something that’s easy to take for granted when targeting the web, but updating binaries running locally on people’s machines is a different beast which requires a lot more coordination. It also can be disruptive to people who are actively on a Tuple call. 

  4. Better late than never! See this issue on their JS SDK. 

  5. After binge-watching Drag Race during COVID, I named the service the “Realness”. If you’re a fan of the show, you’re probably gooped and gagged. 

SSO should be table stakes

On its face, SAML-based Single Sign-On (SSO) is the perfect feature to push your bigger customers into your enterprise tier.

Your small customers won’t care about it, but your bigger ones are often required to use it by their security departments.

If you’re a new SaaS founder and you want to maximize your revenue, I recommend you create an enterprise tier, put SSO in it, and charge 2-5x your normal pricing. Even with no other benefits, some customers will be forced to choose this option.

People will get a little mad at you, but not much, because just about everyone does this.

(Another reason this move is so popular is because offering SSO costs close to nothing after a little automation, so this price increase is all profit.)

When we were baby bootstrappers, we did exactly this. We put SSO in our enterprise tier, charged ~2x for it, and made a bunch of money.

The thing was, we always felt kind of gross about it.

SSO support is a critical security feature. As the experts at Latacora say:

You need an inventory of your applications, a single place to disable access for users, and a way to force 2FA in as many places as possible. The alternative is madness. Every CSO we’ve asked has SSO in their top 5 ‘first things they’d do at a new company.’

Withholding SSO from our customers made their organizations demonstrably less secure. We could have flipped a switch and fixed this at ~no cost to ourselves, but instead we charged a huge premium for it. This always felt a little gray hat to me. Not quite “I’ve encrypted your data and demand you send me Bitcoin,” but not something I’d be proud to bring up at Thanksgiving.

Back then, we were trying to make sure our company survived, and every dollar mattered to us (particularly because we’re self-funded). We held our noses and did the thing because it was highly profitable and everyone else was doing it.

Fortunately, our business is now profitable enough that we can stop making this crummy tradeoff.

As of today, we’re adding SSO to every Tuple plan at no additional charge.

In the short-term, this decision will almost certainly cost us money.

In the long-term, we hope to attract customers who appreciate our prioritization of security over chasing every dollar.

Already, this decision has had a pleasant side-effect – it’s forced us to offer better benefits to entice customers into our enterprise tier: service level agreements, active user pricing, custom terms of service, tiered discounts, and better auditing and control. We’ve still got work to do here, but I’m excited to sign up customers who are attracted to these features, rather than repulsed by the idea of less secure user accounts.

If you’re a Tuple customer, you can find instructions for enabling SSO for your team here.

If you’re not a customer, please consider becoming one to help replace those sweet enterprise dollars we likely just incinerated.

New Tuple features in v0.96

We just shipped v0.96 (download link if you need it).

Here’s what’s new.

1. You can now ask to join in-progress calls!

Request to join an in-progress call

If your colleagues are in a pairing session, you can now request to join them without trying to get their attention through Slack.

They’ll get a macOS-esque prompt that looks like this:

Request to join an in-progress call nudge

If they accept, you’ll join the call.

2. We added a Health Check to help diagnose call issues

Health Check

If you’re having issues on a Tuple call, it’s nice to know whether it’s our fault (I’m sorry! Please tell us.) or due to a flaky connection.

With this update, you’ll be able to see live info about your Tuple call.

The stats we’ve chosen to display will likely need some fine-tuning, so we’re eager to hear your thoughts about how they can be improved.

3. You can now request your pair share their screen or webcam

Ever been on a call where your pair has forgotten to share their screen? And they’re happily talking away explaining what they’re doing, so you feel slightly awkward interrupting them?

Me too. So we added this:

Request to join an in-progress call

4. We removed our Ably and Cloudwatch dependencies.

This shrinks our binary size by 11MB and reduces Tuple’s bandwidth usage.

Man, I love writing that sort of thing in release notes.

5. We shipped some smaller stuff

  • We believe we’ve fixed a bug that caused the shared screen to have extreme latency after long periods of inactivity.
  • Dragging and scrolling now works in native apps that require deltaX and deltaY attributes (like Xcode’s View Hierachy).
  • Thanks to a suggestion during our last security audit, we now use a PKCE authentication flow.
  • You can now see which account you’re signed in with by clicking your avatar in the top-right of the popover.
  • Removing a friend is now reflected in the UI immediately.

Feedback welcome! Happy pairing :)

Helping with Hacktoberfest

When we set out to build Tuple, we recognized that we could only do it with such a small team because of the generous OSS developers that had paved the way ahead of us. Fantastic tools like WebRTC and Rails likely saved us years of development time.

We’ve been inspired by Digital Ocean’s Hacktoberfest, and we’d like to join them in giving back to the OSS community. Here’s all the goodness we’re rolling out in October to give back. 🥳

Open source teams get Tuple for free (forever)

Maintaining open source projects can be an arduous task. Doing it alone makes it even harder.

We want to ease the burden a bit by providing Tuple to any open source team that needs it.

Want to knock out that one lingering issue in your backlog that’s a bit too complex to tackle alone? Are your docs getting a bit stale and in need of a documentation update pairing party? Whatever you’re feeling, we’ve got you covered.

Fill out this form with some basic information about your team and we’ll take care of the rest.

Extended Tuple trials from October 1st - 15th

For the first two weeks of October, we want to help you make your Hacktoberfest contributions by extending our free trial period from 14 to 31 days. This gives you the entire month of October to pair with a fellow dev on your contributions (tip: check out git-pair to share credit).

If you need some help getting started with pair programming, check out our official Pair Programming Guide. It’s full of practical advice that will have you submitting your best work to the open source projects that need it.

Finally, we’d love it if you’d share the open source PRs you made with @tuple! We’re excited to see wait what you can contribute with a pair at your side 💪 .

We want to sponsor your OSS

Tuple wants to join GitHub in helping to make open source more sustainable through their GitHub Sponsorships program.

We’re going to be selecting a few open source contributors each quarter to sponsor, and you could be in the first round!

Fill out this form, follow @tuple, and we’ll announce the sponsors on Twitter by October 7th.

Video sharing enhancements

Sharing your video on Tuple is a great way to enhance communication while pair programming. Tuple supports using the built-in camera, connected webcams, and as of our last release, virtual webcams!

Here are three new ways you can customize and upgrade the video you share:

1. Use your phone for higher quality video

If you’ve got an iPhone or iPad, you can use them as webcams with the help of a super useful app called Camo. (The camera on your iPhone is quite a bit better than most webcams you can buy, so this is a great option.)

Camo lets you adjust the camera lenses, lighting, colors, zoom, crop and focus via your computer. It’s easy to use, and cleverly pushes a lot of the video processing to your phone to reduce load on your computer.

With the recently-added virtual camera support in Tuple, we now support apps like Camo.

To celebrate, Camo is kindly offering a discount on pro licenses. If you want more control of your image, and hi-res video, give it a try.

Using Tuple with Camo

2. Share almost anything as video via virtual camera

Support for virtual cameras opens the doors to all sorts of video sources. You can now use virtual camera generating software to mix in various inputs, including video captured by DSLRs.

For example, in OBS Studio you could choose your DSLR as a video source for a virtual camera, and then select that as the video you share in Tuple.

3. Add a filter with Snap Camera

Ever wanted to pair program with a bearded dragon? You could add Snap Camera as a virtual source in OBS Studio. Go ahead and use your favorite filters to have even more fun while you work together.

Using Tuple with Snap Camera

Virtual camera support brings new ways to use the devices you already have to share video how you want to on Tuple. We hope you enjoy these new options, and have as much fun using them as we have!

2020 Year in Review

First, a brief 2019 recap

We launched Tuple (a remote pair programming app for macOS), in January of 2019.

We began with a closed alpha of ~12 teams, and invited new cohorts from our waiting list every month or so. The app was invite-only until October, when we finally launched a self-serve sign up flow. (If you’re an indie hacker thinking you need to build out a sign up process early on, please consider that we hit five figures of MRR without so much as a pricing page.)

Overall, 2019 was about rapidly shipping basic, tent pole features: webcam support, 3-way calls, dual cursors, a marketing site, and similar.

At the end of the year, the team was still just us three cofounders, and the business felt fairly simple.

And then, 2020

The Ides of March

When we founded Tuple in 2018, it felt clear to us that remote development was on the rise. We figured the market would grow a bit each year, and we could create a nice little business serving this small but growing segment.

By the start of 2020, it looked like we had been correct: we had hundreds of happy, paying customers. If you’d asked me how the business was doing, I would have described our growth in enthusiastic terms.

Then, March happened.

Suddenly, the percentage of developers working remotely rose to “approximately everyone.”

For us, several things blew up at once:

  • Our largest customer told all of its thousands of developers to immediately start working from home, and to use Tuple to support their frequent pairing practice. The intentionally-naive architecture we used to display the list of online users promptly fell over, taking the service offline for nearly everyone.
  • What had been a slow trickle of inbound inquiries for our Enterprise plan turned into a torrent, and I was suddenly staring at an inbox full of leads that wanted to go through a full sales process.
  • Our support volume tripled.

And so, we scrambled:

  • We dropped everything to focus on the scaling issues. We spent a hectic day pushing out hotfixes and constantly refreshing performance graphs and background job queues. If you’d like a full description of that crazy day, check out this podcast episode.
  • We hired a part-time sales person to take that responsibility off my plate (thank goodness).
  • We started devoting a large chunk of time to getting through the support queue each day. Later, we hired part-time help here as well.

Fortunately, by the end of the month, we felt like we had things under control. We’d hired where needed and made our infrastructure more resilient. The business itself was also healthier: in March, we added more revenue than we had in the 14 previous months combined.

It feels weird to acknowledge that we benefited from something so terrible, and I’d trade this reality for one in which Covid never happened in a second. That said, I’m grateful that we were able to provide something that made this year a little easier for folks. We get messages nearly every day saying that pairing over Tuple has made mandatory working from home a bit more tolerable, and that feels great.

New Tuplers

We added three awesome people to our team in 2020:

  • A full-time macOS engineer, Mikey.
  • A part-time sales person, Adam.
  • A part-time support person, Lito.

In addition, we retained security and QA firms to check our work.

Honestly, I had been skeptical about hiring until this year. We’re programmers wielding the awesome power of automation through code! Surely we could get by with just three founders, right?

Sure. Kind of. Except for two things.

First, we were getting close to burn-out. The huge influx of customers in the Spring didn’t just stress our infrastructure and support process, it added mental stress. Tens of thousands of people were suddenly relying on Tuple to get their work done every day. Bugs in production could mean widespread disruption, and we started to feel anxiety around shipping new things. We were also feeling the pain of the tech debt we’d created in 2019, and taking on the necessary refactors was similarly scary.

Second, while I intellectually grasped that adding people to our team would improve our capabilities, I hadn’t truly internalized just how big a difference it could make: Mikey, Adam, and Lito have improved our product and company.

Just one example: they’ve taken difficult work off our plates and done it better than we could. The parts of the sales process that drove me crazy barely bother Adam at all. Mikey dove into our large-scale refactorings and rewrites without fear. And Lito cranks through our sizable support queue while remaining pleasant and helpful.

On top of that, working with these folks has made our day-to-day experience far more fun. Turns out working with great people just makes things straight-up more enjoyable. Consider me a convert to the power of adding awesome people to your team.

Debt payoff

If 2019 was about quickly shipping the first version of lots of features, 2020 was about coming back to battle-harden those things.

In 2020, we shipped near-complete rewrites of huge chunks of the Tuple client:

On top of these large-scale rewrites, we fixed a ridiculous number of bugs and crashes (turns out a real-time streaming application written in C++ that interacts with native OS APIs is, well, a bit tricky). We add more automated tests each month, and each production release is now reviewed by our QA firm before going live.

Fortunately, this hard work has paid off: we’re seeing fewer crashes than ever, and our median call quality rating is a preposterous 5 out of 5.

To be completely honest, I wish we’d shipped more large-scale user-facing features in 2020. But with our cleaner codebase, I think we’re well poised to do a ton of it next year.

A bright future

I think 2021 is going to be a great year for Tuple.

We’ve got a strong team, a clean codebase, and infinite runway. I can’t wait to tackle some of the larger projects we’ve been dreaming about for years now.

Also, I’m having a great time working on this company. My cofounders are amazing, and our team just keeps getting stronger. What’s more, our customers continue to blow me away with how kind and helpful they are. I’m so glad I get to work on something that’s attracted such a great group of users.

I’m excited about the future, and I hope you are too.

Thanks for reading!

Three-player improvements

We’ve taken some big steps toward improving the experience of three-way pairing sessions on Tuple.

Observers can now control the remote machine (and more)!

The increasingly-inaccurately-named observers now have way more abilities:

  • Remote control with keyboard and mouse!
  • Drawing on the screen.
  • Sending text to the host’s clipboard.
  • Triggering the all-important celebration confetti.

There are still a few restrictions that differentiate an observer (the third person to join a call) from a guest (the second person to join), but those are next on the chopping block. Soon enough, we’ll remove those differences entirely, and a call will simply have a host and multiple equally-capable guests.

Dual/multi-cursors work with all pointing devices now

To support our new three-way pairing sessions, we needed a mouse mode that didn’t feel crazy with three participants.

We decided to expand our dual cursor mode into a multi-cursor mode, with one cursor for each participant.

As part of that refactoring, we rewrote the low-level code, fixing some long-standing bugs that prevented some mice/trackpads from using this nifty mode.

Everyone can see who is in a three-way call

All participants can open their Tuple popover and see everyone who’s on the call.

Hosts can kick an observer from the call

Just in case, you know?

Smaller changes:

  • We removed the Highlight click mode. It’s been our least-used mode for a while, and with the new multi-cursor mode, it really felt like it wasn’t pulling its weight. If you really miss the old highlight circles, you can still trigger them by right-clicking when you don’t have cursor control in tag team mode.
  • We eliminated some crashes that could occur when your audio device becomes unavailable.
  • Mouse events are sent to your pair(s) more efficiently.
  • The UI no longer blocks while the webcam starts up.

Tuple's Big Refactor (v0.79)

Earlier today, we released version 0.79 of Tuple with massive under-the-hood changes:

diff stat of main PR

In it, we overhauled Tuple’s low-level signaling code (responsible for handling connections between clients).

This has a few user-facing benefits:

  • Snappier UI responses due to better use of threads.
  • Two users calling each other at the same time will now successfully connect.
  • Fewer availability bugs (like not seeing someone online when they’re connected).
  • Fewer crashes.

This also has some great things for us as Tuple’s developers:

  • Lots of new automated tests.
  • Code that’s far easier to reason about.
  • Better decoupling of UX and underlying signaling code.
  • A much better foundation on which to build our next set of features.

If you’d like to hear more of the details, I recorded this walkthrough with engineer Mikey:

We’re excited to have this release live, but even more excited by what it’s going to let us build in the future.

Two other quick items:

  • We have a beta version of a Slack integration! Now you can /tuple @coworker to kick off a call from a DM. Please give it a try and let us know how it can be better.
  • We’re dropping High Sierra support from Tuple on November 1st, when Apple is expected to stop issues security updates for it. You’ll need to update your OS before then if you’d like to keep using Tuple.

If you’re already a Tuple user, you’ll receive this update automatically (or can download the latest version here).

If you haven’t created a Tuple account, you can sign up for a free trial here.

Tuple's August Release & Other News

Quick summary

  • We’re hiring a WebRTC expert to bring mobbing support to Tuple. Please apply or share the job posting with someone great.
  • We added some new features: an (optional) persistent indicator showing which display you’re sharing, we now exclude the webcam window from your screen share, and made a bunch of small improvements to make Tuple more Mac-y.
  • SSO users: you no longer have to sign in after every app restart. Woohoo!
  • We’re dropping High Sierra support in November, when Apple is expected to stop issuing security updates for it.
  • We have some solid improvements in the pipeline: fewer crashes, better automatic call reconnection, better support for 3-person calls, and some onboarding improvements.
  • If you could use someone to pair with, you can get $40 off your first Codementor session by signing up through this link.

A short hello from Ben (and a favor request)

News items

We’re hiring a WebRTC expert (and would love your help)

We want to hire an experienced WebRTC programmer to help bring mobbing (meaning pairing sessions with more than 3 participants) to Tuple.

If this feature is on your wish-list, you can help out quite directly by referring awesome candidates to our job posting.

We’re moving to a monthly release cadence

A number of you have requested that we update the app a bit less frequently. We’re going to test out doing monthly releases for a while to see how it feels.

So far, so good.

We’re dropping High Sierra support on November 1st

November 1st is when Apple is expected to stop offering security updates for High Sierra.

We think it’s important to only run Tuple on secure systems, so we’ll disable the ability to run Tuple on High Sierra near that date.

This change affects less than 2% of you, but we wanted to give you plenty of notice anyway.

We’ve partnered with Codementor so you can pair with experts

Need someone to pair with? Want to get a quick tech question answered by an expert? Codementor is a great way to bring in short-term development help.

As a Tuple customer, following this link will give you $40 in free credits to use on mentoring sessions or freelance jobs. (Any unused credits will expire in 30 days.)

New stuff in Tuple

We pushed v0.78 today. Here’s what’s new.

More Macification

Thanks to our Mac expert Mikey, Tuple has been getting more and more Mac-y. Here are a few small examples:

  • More of Tuple’s sub-windows (like preferences or other models) now respond to standard macOS shortcuts like Command+w and Command+`.
  • Tuple now shows up in the Command+Tab results in a more predictable way.
  • You can now select a call rating by double-clicking the rating emoji or by pressing Command+[a number between 1 and 5] (and then Command+Return to submit).
  • Previously, if you were trying to type on a remote machine but had your mouse outside the Tuple window, we wouldn’t send your keystrokes. That wasn’t very Mac-like (it was more X11-y, really), so we changed it. Now, your keystrokes will be sent whenever the Tuple window has focus (provided you haven’t disabled this in the guest toolbar).

New: display sharing indicator

Lots of folks have told us that they love how unobtrusive the Tuple UI was, but said maybe we’d gone just a touch too far and should have a persistent element that makes it super clear when you’re sharing your screen.

We agreed, and added what we hope is a clear, but still-unobtrusive border:

screenshot with red border

This feature is on by default, but you can disable it in preferences if you prefer Tuple to run in stealth mode.

New: webcam sharing exclusion

If you’re sharing your screen, and someone shares their webcam with you, it creates a weird situation where they’re forced to watch a slightly-delayed version of their own webcam feed on your screen.

That’s distracting, but it’s also bad for CPU and bandwidth usage, since Tuple needs to capture/encode/transmit a live video feed as part of your screen share. Higher resource usage and (likely) more latency. Ew.

To get rid of this unfortunate situation, we now magically exclude the webcam window from your screen share.

In this first screenshot, I’m sharing my screen with Joel, and he’s turned on his webcam. This is what I see on my actual desktop:

screenshot with red border

And here’s what Joel sees: screenshot with red border

Poof! No webcam.

One really cool detail: since your pair might click on the webcam window and not know they’re doing it, we automatically send their clicks through the window so they can interact with elements underneath it as normal. Kind of slick, right?

Bug fixes

  • You might not know this, but there’s a Tuple preference to persist all drawings on the screen until their creator has right-clicked. This used to only work for guests, but now it works for hosts as well. Paint away, friends.
  • SSO folks: you should no longer have to sign in after every app restart. Sorry you had to live with this for so long.
  • When you’re using the Tag Team mouse mode, there is just one mouse cursor, and each person takes turns using it. To get control when you don’t have it, you click one time. For guests, we’ve always swallowed this first click so you don’t accidentally click something when you’re just trying to get control. Now, we do this on the host side too.
  • If you’ve enabled the preference to automatically start a webcam feed when calls begin, this will be respected for audio-only calls too.

Upcoming improvements

Here’s a sneak-peek at what we’re shipping soonish:

  • Fixes for our most common crashes.
  • Far fewer dropped calls.
  • A fix for the issue where two people calling each other at the same time can lead to unpredictable behavior.
  • A better first-run experience for the app.
  • Better support for 3-person calls (observer mode). Currently, observers can’t draw on the screen, and the presence of an observer adds surprising limitations on the other call participants. We’re going to do a pass on this feature to flesh it out and make it more useful.

Until next month!

Redesigning the Tuple Client UI

After two years of building Tuple, the time has come to redesign our Mac client. We’re very excited to solve many usability problems that have cropped up over time and for our aesthetics to reflect the quality of the Tuple internals.

This post is a deep dive into how we planned, executed, and shipped the new design.

Shaping it up

Since this was a rewrite of the front-end codebase, we planned diligently to ensure scope did not creep too far. First, we took a page out of the Shape Up methodology and kicked things off with a pitch by our CEO, Ben.

Here’s an abridged version of the pitch that Ben wrote up in a Notion document:

### Problems

The Tuple client (the installable Mac application that lives in the menu bar) 
has sub-par UI/UX.

The following issues are paramount:

- People don't realize they can add an observer to a call 
- People don't realize they can swap roles easily
- The changelog is hidden
- The "End" button is unclear
- Call controls are not clear to all users
- Inviting new users is hidden away and confusing
- Team directory is maybe not necessary
- Online status not clear

### Appetite

Let's keep this project to 4 weeks.

After reviewing the write-up, Ben and I hopped on a quick call to talk it through and fill in the gaps. My goal was to learn what items were most pressing and risky, in case we had to cut scope to stay within the 4-week budget.

Next, I jumped on a Tuple session with Spencer to talk through the technical details. I learned the interface is a React app that gets rendered in a Safari WebView and communicates with the Swift app through a JavaScript bridge. The Swift app is in charge of managing state, passing that state to the WebView, and listening for various commands issued from the React app.

Fortunately, the boundary between the React and Swift apps is quite clean, so we decided to build the new front-end in a separate codebase and compile it into the native codebase during the build phase.

Setting the foundation

The original codebase was clean and well-written, but it was beginning to show its age. It was mostly written in the pre-hooks era, so all the components were implemented with classes. CSS styles were embedded directly in the JavaScript, but we knew we wanted to utilize Tailwind CSS & Tailwind UI moving forward. The changes we wanted to make were significant enough that I decided to build new components from scratch instead of adapting the existing ones.

I chose to start with the main home screen to develop the new visual identity:

Old Main Screen

After sketching a bit with pen and paper, I cracked open my editor and created a new project. I prefer to prototype with HTML/CSS as early as possible, so I skipped the typical phase of prototyping in Sketch or Figma. Tailwind enabled me to style rapidly with utility classes.

Here’s how the main screen shaped up:

New Main Screen

Connecting with people

Connecting with teammates and friends on Tuple used to be quite confusing. In the old interface, users had to click the gear icon to open a dropdown menu, where they could either choose to “Invite Teammate” or “Add Friend”:

Old Invite Flow

These two options sound confusingly similar, and customers often picked the wrong one. As someone quite familiar with the app, even I had to study the data model for a while to grasp the nuance.

Ultimately, we decided it was best to unify these two forms under one view. Instead of forcing the user to choose upfront, we now funnel everything related to connecting with people through one interface: the “Connect with people” form.

New Invite Flow

Smoothing out the in-call experience

The team identified a handful of UX issues with the in-call view:

Old Call View

Here’s how the new in-call view turned out:

New Call View

Sharing progress with the team

This interface has a lot of different possible states – by the end of the redesign, we were handling over 25 distinct permutations. Fortunately, it was easy to simulate each state by passing mock data to the React app.

Our <App> component has two main props: a state object representing the state of the application, and a cmd function that dispatches commands back to the Swift application. Each time I designed something to handle a different permutation, I added a new instance to our demos page backed by a new state object in our /fixtures directory. Gradually, the page grew to represent every possible unique view users might see.


I also implemented a mock cmd function that would simulate changes to the state object, so the team could click around the app and get a feel for how it would behave in real life. For example, clicking the “Call” button would put the app into an “outbound calling” state and then, moments later, flip to the “in call” state to simulate someone answering the call.


After about a month of development and iteration, Spencer plugged the new interface into the Swift application and pushed it live behind a feature flag. We chose a few of teams to beta test and give us feedback. This process unearthed a handful of small bugs and points of confusion, once again proving it’s impossible to anticipate every scenario that could use improvement.

We hope you enjoy the facelift and find Tuple app even easier to use!