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

By Sophia Westwood

This two-part post covers how we enabled third-party developers to embed apps inside of Quip documents via the Quip Live Apps platform.

In 2016, we hatched a vision at Quip: make it possible to embed anything into a Quip document, and allow developers around the world to extend our document canvas. We knew it would be incredibly challenging. On the technical side, we anticipated issues around managing iframes, setting up a security model, sandboxing third-party code, and handling performance. On the usability side, we would need to make it easy for a developer to build an app. Then it had to all come together and feel natural for the end user.

Fast-forward to Dreamforce in November 2017. That's when we launched Live Apps, a platform for developers to build apps that embed into Quip documents. Through the collaboration API, developers can leverage automatic support for real-time syncing, editing, and commenting, available offline and cross-platform by default. We also launched first-party apps such as calendars, polls, and Kanban boards, and open-sourced the code for reference.

What does it all look like at the end of the day? As an engineer starting a project, it's normal for me to write a Quip document containing the technical design in code snippets, the task list in checklists, and the launch metrics in embeddable spreadsheets. I'll add a calendar for the project timeline, a poll for team lunch, and a live usage graph to drive some urgency. More broadly, Live Apps make it possible for teams to pull in data for visibility from integrations with Salesforce, Jira, or custom internal tools.

Visions of an extendable document date back to OpenDocActiveX, OLE, and even Xerox's Alto. With Live Apps for Quip, we took the opportunity to go further and build something designed for the mobile, online, and social world today.

Challenges in building a platform

At their essence, Live Apps are third-party code inside iframes in Quip documents. Apps store their data in Quip, and they can optionally access the wider internet (in a tightly-controlled way). App code communicates with Quip code via APIs that form a bridge between the iframe and the main window. Apps cannot access document contents by default, and admins determine which apps are available to site members.

Over the course of the project as a whole, here's a selection of the conversations that had to happen:

  • “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 doesn't feel natural.” – How do we make third-party apps feel like first-class page members? How do we make them resize dynamically like other Quip content? How do we avoid content clipping when UI components (such as menus) extend past the iframe bounds?
  • “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?

In this two-part post, we're going to step through the first three challenges: Make it easy for developers, build a platform (iframes are scary), and make it collaborative. We'll show how we built the platform and talk about the foibles we encountered iframing third-party code.

Make it easy for developers

We wanted to make it shockingly straightforward for developers to get a Hello World app running inside of Quip. First, we allow anyone to make a Live App. All that's needed is a Quip site, available via the Quip signup process. From there, it's a one-click button on the Quip Developer Console to create a new app with a fresh new id.

Next, we needed a way for people to create and upload app bundles. Looking around at other tools, create-react-app stood out as a model example of easy setup. It reduced complexity to a single command and required no configuration.

We built an analogous create-quip-app NPM module so developers could immediately start on the Quip platform without worrying about configuration.  create-quip-app depends on quip-apps-webpack-config, our module to centralize a standard webpack configuration. Shipping our own standard webpack config meant we could by default include babel-polyfill, meaning apps would support modern JavaScript while remaining compatible with older platforms like our Mac app for Yosemite.

After installing the module, running the single command create-quip-app hello-world creates a folder like so:

hello-world
├── package.json
├── node_modules
├── webpack.config.js
├── app
│   └── manifest.json
└── src
    └── App.less
    └── App.jsx
    └── root.jsx

The bare minimum needed at this point is to copy the id provided in the Developer Console, run one more command (npm run build) to package the app, and upload the outputted .ele file via the developer console (exact details in the Getting Started Guide).

That's it. The app is actually up and running at this point, available for the developer's Quip account. It can be added into a Quip document by name via the @ insert menu, just like all other apps.

Build a platform! (iframes are scary)

Now let's dive into what powers the developer experience: the platform.

Almost all platforms require three things to support third-party apps: configuration data, library code, and app code. In addition, some also support storing app instance data. Here's what that breakdown looked like for us:

  1. Configuration data: The name of the app, required API version, toolbar color, and so on.
  2. App code: JavaScript, CSS, and other assets to render the app into the iframe.
  3. Library code: Quip code to handle communication with the surrounding document.
  4. App instance data: A way for an app instance to store long-lasting data in Quip, such as specific poll options and votes.

Stepping through each in turn sheds light on the life of a Live App and a few of the security considerations along the way.

Configuration data

We chose to centralize configuration data in an app manifest, a simple JSON file that describes the app. The file gets generated when the developer first runs create-quip-app. Here's a basic manifest file, the one we use for the Poll app.

{
    "id": "JYKAjAYyzLM",
    "name": "Poll",
    "toolbar_color": "blue",
    "js_files": ["dist/app.js"],
    "css_files": ["dist/app.css"]
    ...
}

We also use the manifest as a place for developers to whitelist the external web requests that their app is allowed to make. By default, we block all other external requests that come from the iframe.

Security is one reason for this whitelisting. Preventing apps from making external requests helps sandbox the app by making it much harder to interact with the outside world. For example, it is one line of defense against malicious developers looking to exfiltrate data from app instances.

Product is another reason. We want to be able to identify which apps are fully functional offline. Quip itself works both online and offline, but apps that load in content from the outside world might not be. By requiring developers to explicitly list these cases, we can easily distinguish between apps that are fully functional offline and apps that aren't.

If an app needs to define certain whitelisted URLs, that happens in the manifest:

"csp_sources": {
    "script_srcs": [
        "https://foo.com",
        "https://bar.com"
    ]
},

We read the manifest to determine the Content Security Policy (CSP) for the app. In this example, we generate the CSP header value "script-src https://foo.com https://bar.com;"  for the header Content-Security-PolicyX-Webkit-CSP, or X-Content-Security-Policy depending on the browser. Now the app can load scripts from foo.com and bar.com only. Apps can similarly whitelist URLs for font_srcs, img_srcs, connect_srcs, and so on to load other resources or allow AJAX calls to external domains.

App code

When an end user chooses an app to insert, we create an iframe pointing to a special URL to load the app code:

https://element--k-o-r-aj-a-y-h-e50.quipelements.com/-/element/view-frame?hv=6&element_config_id=KORAjAYHE50&retina=True&seq=172&api_bundle=6ALan3xqd8N9k-3XkBNmMg

There are a few important decisions that went into this URL from a security perspective.

First, the URL is served from a separate domain (quipelements.com) instead of Quip proper (quip.com). Separating the domains helps sandbox the third-party code from the Quip document (cookies, for one) and protects against attempts to impersonate first-party code (for example, by making server calls).

Next, each app has its own subdomain. Same-origin policy dictates that iframes loaded from separate subdomains cannot access each other directly. For Quip, that means that an app from one developer can't access the JavaScript context or DOM of an app from another developer, even if both are rendered inside of the same Quip document.

The iframe uses the app subdomain to fetch the app's resources, including the JavaScript code that instantiates an instance of the app. Note that this process is more complicated in our native Mac, Windows, iOS, and Android apps, where we intercept the URL request and send back cached resources in order to still support apps when offline.

To be continued...

We're going to end on that cliffhanger and pick up the narrative again next week. So far, we've covered “Make it easy for developers” and moved halfway through “Build a platform (iframes are scary).” In part two next week, we'll jump into how the library code handles communication between the app and the host, and discuss how the platform supports long-lasting, custom, live-syncing data. Then we'll end on “Make it collaborative.”

This is Part 1 of a two-part series. Part 2 will be published soon!