Plain changelog
Plain changelog

User Statuses

👉 A quick housekeeping update: We're moving away from weekly updates, to posting in-depth updates as and when they make sense to share.

This week is all about user statuses! We had previously done some of the API work for this, and now it's available in the client too. You can now switch your user status quickly and easily from the top navigation. Also, you can now see all of your teammate's statuses in the timeline, customer queue and everywhere else. This makes it easy to see if your teammate is online before assigning them a customer or taking any other action.

As part of this, we've reorganised our navigation to split out workspace-specific settings from your personal settings. This makes it a bit clearer as to what options are specific to you, versus your workspace.



  • 😌 If you mistakenly mark a customer as helped, you can now undo it in one click. No stress!
  • 📍We improved our assignment UI so that popovers now open in-location rather than centered on the screen. Thanks Radix!
  • 👆We improved our focus states app-wide so that we now only show a focus state when you are tabbing through the app (thanks to the magic of :focus-visible).
  • ⏱Customer TimelineInfo now has an updated_at timestamp like all other entities in our APIs
  • 🚂We've made various improvements to how we use subscriptions and cache customers in our Support Tool so everything is as snappy as possible and all customer information is updated in real-time via GraphQL subscriptions.
  • 😎We added a new GraphQL query called myUser to our core API to fetch your own user when you are in a workspace. This means you no longer have to first get your account and then from there fetch your user!
  • 🪄We made loads of content changes and improvements throughout our Support App. Much to the satisfaction of one engineer on our team who shall remain nameless, you no longer "Log-out", you now "Sign out" instead.


  • Customer counts were not correct in the customer queue for some filter combinations. Now you can count on them always being right.
  • We fixed some validation on our "mark as read" mutation in our core API





Weekly Update — 20 September 2021

We've been taking on fairly large slices of the platform and product for a few weeks in a row so this last week was spent mostly wrapping up loose ends and making small improvements here and there.

We've also been hard at work on our chat widget, but it's not quite ready for demoing yet!


  • 👀 Timeline entries are now marked as read more granularly: If you only scroll only 3 of 5 messages into view, only those 3 will be marked as read.
  • 🏃‍♀️ To make chat feel super snappy, we now optimistically update the timeline when a chat message has been successfully sent. This makes it feel a lot faster and cuts out some ~300-500ms from the time it takes for the timeline to update from pressing send on a chat.
  • 🔄 We improved the Support App's scroll handing in the timeline to make pinning to the bottom (e.g. chat-style inverted scrolling) more reliable. Turns out… this is a Hard Problem ™️
  • ✅ We’ve also improved our handling of a number of edge cases and errors in our GraphQL subscription infrastructure.


New & shiny

  • ✨ We now have a new GraphQL subscription to subscribe to a specific customer. In the future, this will be used to update a customer's status or their details when they change.



Let there be Subscriptions!


☢️ Warning this is a technical one!

We want our support app to feel super fast and importantly make collaboration between team members really easy. Like for any productivity app, an important part of this for us is making sure all UI updates are in real-time. We want to make sure you know instantly if an advisor starts to reply to a customer or issues are opened or closed by a back-end system.

This week we concluded the first big step in this direction by implementing GraphQL subscriptions for a customer’s timeline. It's hard to screenshot live-ness since… it's the same UI but … live. So instead we wanted to share a little on how this works behind the scenes, and specifically some of the technical challenges of building this in our architecture.

Typically GraphQL subscriptions are over WebSockets. Handling many WebSockets connections at scale is not a trivial thing to architect and implement yourself on traditional architectures. This is due to the connections being persistent and not stateless like HTTP requests (hence why there are so many real-time SaaS providers out there!).

Since our current architecture is primarily using AWS serverless solutions like AWS API Gateway, AWS Lambda, DynamoDB, and Eventbridge, we decided to try to implement a scalable GraphQL Subscription over WebSockets solution using these serverless technologies. The main hurdle we encountered was that the open source NodeJS GraphQL libraries all assume a long-running stateful server holding on to a WebSocket connection. This doesn’t exist with AWS API Gateway and Lambdas, so it meant that we needed to significantly reengineer how these libraries handled WebSockets.

The rough outline of our solution is as follows (this deserves a longer more detailed blog post): When a client subscribes to a customer’s timeline, it establishes a WebSocket connection to AWS API Gateway. This invokes our Lambda function with a connection id and the payload of the message. The connection id and the GraphQL subscription then get stored in DynamoDB table. When a new timeline entry is added to a customer’s timeline, then an event is fired and an event handler Lambda queries the DynamoDB table for WebSocket connections that are subscribed to that customer’s timeline. If it finds any it then executes the GraphQL subscription with the event as an argument to get a result. Finally, we send each connection the GraphQL result and… we're done! Phew.

That's the happy path, but with a host of detailed connection and performance issues to handle this was not trivial to implement. Also, for a front-end to really feel live, it had to not only be told of new entries (the easy ones) but also when an entry was updated or removed and it had to handle out-of-order or duplicate entries.

This is the final schema we ended up with:

  enum TimelineEntryChangeType {

  type TimelineEntryChange {
    changeType: TimelineEntryChangeType!
    timelineEntry: TimelineEntry!
    cursor: String!

  type Subscription {
    timelineChanges(customerId: ID!): TimelineEntryChange!

It's been really challenging but we're really happy with this first implementation - we've built a lot of infrastructure that is re-usable meaning that in the next few weeks and months we can roll out subscriptions for many other resources such as customers details, agent statuses, and anything and everything that needs to be real-time!

Fancy working on problems like this? We're hiring!





Weekly Update — 6 September 2021


This week, we've been busy advancing on two larger initiatives: introducing support for GraphQL subscriptions, and progressing on our client-side chat widget. We're excited to share more on both in the coming weeks.

In the meantime, we've made a ton of improvements to our support app, including lots of small but impactful UI updates:

Customer queue: we now show customer avatars, the latest message preview, as well as what's happened since you last looked at the customer, alongside a set of smaller alignment and spacing improvements.


Customer sidebar: Following on from last week's API work here, we now show the last contact channel, the wait time, as well as the unread count in the customer sidebar, alongside lots of other interface improvements.






Weekly Update — 31 August 2021


Chat widget foundations

We've started work on our client-side chat widget — what your customers will use to talk to you.

mock (3).png

Most existing offerings opt for a floating widget which you can integrate via a script tag. Those can be quite handy and simple to set up, but offer little control when they need to work as part of an existing web app. We're going to instead offer greater developer controls by providing an NPM package where companies can integrate the widget into their UI however they like — for example, by customising individual components, changing how and where it appears, theming it, and so forth.

In the past week, we've built some of the foundations for this:

  • Session token handling — the chat widget now accepts a signed JSON web token that identifies the customer and then exchanges that with via our customer API for a session token.
  • Receiving and sending chat messages — we now render chat messages and let your customers respond. Gotta start somewhere!
  • Marking as read – The widget now correctly marks messages as read… when they are read. This is important for notifications!


Other changes

  • Via our API you can now get summarised timeline stats per customer. This, for now, includes how long they have been waiting for a reply, what channel they last used to get in touch and also an unread count for all activity in the timeline.
  • API support for filtering customers by last contact channel, whether they are assigned to anyone, and by a specific assignee.





Weekly Update — 23 August 2021


Customer queue

We've built our first iteration of queues in our API and App. It shows you all customers who you are currently helping as a team broken down by status and assignment.

Screenshot 2021-08-23 at 13.54.22.png

As our very first pass of this, it's very limited in what it currently supports - in the next few weeks we plan on adding filtering and richer information on each customer such as their most recent message if and how long they've been waiting for a reply and much much more.







Weekly Update — 16 August 2021


In a lot of customer service platforms, an advisor is either online or offline. This doesn't really as a manager give you a great sense of whether someone is just having lunch, is helping a teammate out or… uhh.. doing other things 🚽.

To make this a bit better we allow advisors to explicitly go on a break as well as being offline. In the future, this will us to better handle customers replying or getting back in touch while you are working but just temporarily stepped away from your desk. Importantly it will also be able to feed into how reporting works and give managers a great overview of the team, especially in remote teams and during global pandemics.

 enum UserStatus {


Other changes

  • Feature: There is now a new unassignAllCustomers mutation to unassign all customers from a user. This will be used in cases where advisors go offline for extended periods of time and want to make sure their customers will be looked after by other advisors in the team.





Weekly Update — 9 August 2021


Two customers walk into a sidebar…



We've added a list that shows you all your currently assigned customers in the navigation sidebar, so you can easily jump between customers.

This is a super basic version for now, in the next few weeks we'll be making this a lot richer to show you the channel the user is using, unread counts and give you a general sense of priority!


Other Changes

Besides that this week we kicked off a few larger pieces of work such as GraphQL Subscription support and our embeddable widget libraries - more on that in the future 🤓





Weekly Update — 2 August 2021


It's new website day!


We've got a new website, name and domain, and we're super excited to share it with you. Head over to


Other changes

  • Improvement: Our customer state machine implementation to be a bit sturdier and production ready
  • Improvement: We've sped up our API integration testing — we put a lot of work into making sure our API testing is rigorous and exhaustive. We'll write a blog post on this one day, it’s a super interesting topic!

Weekly Update — 26 July 2021


Slash commands

In a productivity tool, every mouse movement adds time and friction - so we're making sure the Plain app can be used without ever having to lift your fingers from your keyboard.

Inspired by Slack, you can now perform any given action from the composer. Simply hit / and use the arrow keys to open issues, resolve issues, mark customers as helped, and so forth.



Other changes

  • Improvement: Authentication for our customer GraphQL API now enforces the expiry of tokens provided by you. This is to ensure that you never, even by mistake, issue a customer token that permanently identifies a customer.
  • Improvement: We extended our API schema so that we can now attribute a timeline entry not only to a customer and user but also to an internal system. This will help us debug things when they don't quite work and inform reporting. This means that we now have a SystemActor along with UserActor and CustomerActor.
  • Improvement: Related to our state machine work, customer state transitions now show neatly in the timeline so you know what happened and when.
  • Improvement: If you try to mark a customer as helped with open issues, we now open a modal to allow you to resolve all open issues in one click first.