Embedding third-party React apps in Quip for fun and profit – Part 2

Let’s dive back in. Last week, we talked about why we built the Live Apps platform, how we made it easy for developers to get started, and the inner workings of the configuration data and app code (plus some iframe tidbits along the way).

First, a quick refresher on the three challenges we set out to cover in these two posts:

“Make it easy for developers.” – How can we make it easy for a developer to get started building an app?

“Build a platform! (iframes are scary).” – How do we support third-party code inside of Quip? The answer probably involves iframes... but how do we embed third-party apps inside of Quip documents in a way that's secure?

“Make it collaborative.” – How do we expose an API to a data model that supports multiple editors, working online or offline, and commenting?

“It's too slow.” – How do we load the code for five, ten, or twenty app instances, all living in their own iframe?

“Now do it all in embedded WebViews on iOS and Android.” – How do we make it all work cross-platform on web, Windows, Mac, iOS, and Android, where apps not only live in iframes, but in iframes inside of WebViews inside of native code? Customers expect Quip to work natively across all their devices — how do we make Live Apps do the same?

This post will cover the second part of building a platform. We'll discuss how we support communication between the app and the Quip document, as well as how we enable apps to store long-lasting, syncing, custom data that works online and offline. Then we'll end by showing a few powerful collaboration components built into the platform.

Build a platform

Back in Part 1, we separated the platform into four pieces and walked through the first two.

Configuration data: The name of the app, required API version, toolbar color, and so on.

App code: JavaScript, CSS, and other assets to render the app into the iframe.

App instance data: A way for an app instance to store long-lasting data in Quip, such as specific poll options and votes.

Now let's talk about the library code and the app instance data.

Quip library code

The library code needs to do a lot. It provides UI components such as buttons and colors, handles layout, enforces permissions, and provides an interface to data. At a high level, it connects third-party embedded apps with the first-party host document.

This section focuses on how the Quip library code allows apps to exchange information with the surrounding host. Examples of information from the host document are:

The name of the person viewing the document.

The current environment (such as whether the app is being rendered on a phone or on desktop).

Updates to custom data arriving from other people or windows.

Examples of information passed back to the Quip document are:

Custom data the app wants to store (such as poll options or a vote).

Configuration data for context menus or the app toolbar menu.

A request to open a link or trigger a Quip component such as a comment bubble.

The code responsible for this two-way communication is called the bridge.

We implemented the bridge with the standard method for cross-origin iframe message passing: window.postMessage. We chose to define the message format using protocol buffers, a cross-language way of serializing data. We use protocol buffers extensively at Quip. They're how we store and transmit almost all our data. Protocol buffers provide more structure than JSON, with clear definitions and per-language generated source code. The added structure and clarity made them a good fit for bridge communication as well.

Here's a concrete example of a protocol message passed across the bridge. To request that the host open a link in a new window, an app calls quip.apps.openLink("https://www.wikipedia.org/"). The library code then constructs a protocol buffer message in the following format, serializes it, and sends it across the bridge by invoking postMessage.

When the host document receives the message, it runs code to open the URL. Because Quip is cross-platform, documents appear on web, Mac, Windows, Mac, Android, and so on. That means that embedded apps also appear on all those platforms. An app just calls quip.apps.openLink, and the underlying implementation behaves correctly based on whether the app is showing on the Quip iOS app versus web versus desktop.

Tidbit for the curious: This section shows the app code and the library code existing together in a single iframe, but that's actually a simplification. If you were to inspect the actual Poll live app in your browser, you'd notice that the app code and the library code in fact belong to separate iframes. We did this for the sake of performance to avoid loading the large library code again for every app instance, and it involved a whole new dimension of iframe trickiness.

App instance data

The next major piece is app instance data. For example, there might be five polls in a document, all with different options and vote counts. The polls need to be able to store that data somewhere long-lasting. We chose to support storing data via the platform.

Another option would have been to have developers handle data storage entirely on their own, outside of Quip in whatever format they chose. In that case, an app would need to fetch its own data from its own external server each time it loaded. It would be one less thing to worry about from a platform implementation perspective, and developers could use whatever tools and data format they preferred. The disadvantage is the barrier to entry for simple apps. If all someone wants is to store a color preference, a few poll options, and a couple of votes, it's a lot of overhead to set up databases and servers on your own.

Providing data storage via the platform also has some advantages specific to embedding apps inside of Quip. Collaboration is a core part of Quip. Anyone on the document should be able to use an app at the same time, with simultaneous changes syncing as seamlessly as the rest of the document. That's a lot to ask a developer to implement from scratch.

Quip is also multi-platform and offline by design. You can edit anytime on any device, and the changes sync whenever you come back online. Synchronous and asynchronous syncing is also a lot to ask a developer to implement from scratch. Plus, apps that were fully dependent on data from external servers wouldn't load properly offline.

So, we set about figuring out a data model that was both expressive and reasonably intuitive, while also supporting multi-person updates and syncing. This part was pretty tricky.

Thankfully, we already had plenty of infrastructure built out for syncing other types of content in documents. So, we backed app data with the same core Quip foundation we use for regular paragraphs, spreadsheets, checklists, and so on: protocol buffers stored in units called sections, with a syncer handling data integrity and real-time updates. Modeling custom app data as sections meant we could leverage our existing code to handle live syncing, offline, multiple people, and so on (learn more in Bret Taylor's post on building the desktop apps).

When we held an internal hackathon last summer to test out the platform for the first time, we found that our first effort at layering a data API on top of sections was too confusing. Quip engineers built seventeen apps on top of the platform across thirteen teams and four days, asking questions and providing feedback along the way. Through the hackathon feedback, we learned we needed to make the data APIs more intuitive before launching. In particular, we decided to make storing data in Quip conceptually similar to putting data into a nested JSON object.

We settled on using a quip.apps.Record object as the basic unit of data. Each app instance gets a special quip.apps.RootRecord when it's created. Records can have named properties that map to primitives like strings, numbers, and booleans (or objects and arrays containing primitives). Or, they can have named properties that in turn map to other Records or RecordLists. This structure can continue indefinitely in a tree hierarchy.

Some apps might only have a single RootRecord, storing for example a custom color string under a color property. Others such as Calendar might have a more complex record structure, with the RootRecord storing the default month and a list of EventRecords, each with a title, date range, and color. The internal data for Poll is pictured below, and it is even more involved. When the document loads, the host loads all existing Records for an app and sends them through the bridge during initialization.

Zooming out, the big advantage of using Records is that they are synced independently, merge intelligently, and work offline. Because Poll uses Records and RecordLists to store data, changes will sync automatically if two people vote at the same time while a third person adds a new option. Note that it's still possible for developers to pull in app data from external servers or APIs. That's what we did for the Jira and Salesforce Record live apps, using the Quip data model as a cache.

Make it collaborative

Beyond making it possible to embed third-party apps, we embraced the idea of providing an API for collaboration: a neat package of the powerful collaboration pieces we had already built in Quip, made available for anyone to use. For example, the platform data model means developers can build on top of our end-to-end editor technology for live-syncing, real-time collaborative data. Here are two other pieces we worked into the platform.

A core part of collaboration is conversation. It's critical to be able to ask for clarifications on calendar timings, advocate for certain poll options, or celebrate 🎉 when stuff gets done. So, we made it crazy fast for developers to add context-specific Quip-style conversations anywhere in their app. Familiar little chat bubbles show up attached to different pieces of the app. Anyone on the document can click them to leave a comment about that area, just like they would for a paragraph in the document. These comments go through the exact same system as regular Quip comments, with mobile notifications, references to other documents, and search.

To enable these conversations, all the developer needs to do is add one line to tell a Record to support comments, then another line to point to the DOM node involved in the conversation. We provide a React component that takes care of the rest: <quip.apps.ui.CommentsTrigger record={record}/>. The commenting recipe in the documentation has the exact details.

Here's the result:

Another core part of collaboration is shared editing. We wanted it to be possible for apps to not only leverage the same data model as the Quip editor, but to actually embed mini versions of the editor inside of themselves. Editor inception! Inside of this embedded-editor-in-an-embedded-app-in-the-regular-editor, people can use the same formatting, rich @mentions, and live syncing as in the surrounding editor.

To do all that, a developer just needs to add a RichTextRecord (a special type of Record) as a data property and pass it into the React component we built to handle the heavy lifting: <quip.apps.ui.RichTextBox record={record}/>. In the screenshot above, the “Launch!” calendar text is actually a tiny RichTextBox.

Conclusion

There's way more we could talk about, from how we made third-party apps in iframes feel more like natural page members, to the major work required on performance, to the extra challenges presented by mobile. (It turns out that loading 15 heavyweight iframes on embedded Safari for the first time turned out a bit differently than what we hoped for.)

Support

Related Links

We use cookies to make interactions with our websites and services easier and more meaningful, to better understand how they are used, and to tailor advertising. By continuing to use this site you’re consenting to the use of cookies, which you can learn more about here.