Building a world-class POS within a short timeframe

Vue.js

Vuex ORM

JavaScript

Jest

REST API

Capacitor.js

Swift

Appium

Cypress

SASS (BEM)

Shape Up

Cross-functional teams

Figma

TL;DR

0 weeks

From inception to release

0

Operators across 1,400 sites

£0 million

GMV processed

0 million

Orders processed

Built with speed and stability.

Responsible for setting the template internally on how to build with velocity, whilst maintaining exceptional quality, in distributed autonomous teams with Shape Up methodology; delivering major features on time for several continuous build cycles. Within a mere six weeks from project start, we had a fully functional first version of our iOS app on the App Store.

Critical team member.

Responsible for developing and managing key features across all three projects that made up Onvi's final product offering, including iOS app (Capacitor.js) and web apps (Vue.js). Integrating physical hardware, diagnosing and rectifying issues, optimising performance, and providing technical support to design and sales teams.

Close collaboration.

Collaborating with product designers from project inception to completion. Engaging in breadboarding, wireframing, prototyping, and delivering polished UI/UX designs for development. Serving as the team's cohesive force, facilitating timely delivery of key components for overall feature implementation.

Impressive growth.

Since its launch in 2020, the Onvi product suite was used to place over 4 million orders and take £66 million in GMV. We secured some big names in the hospitality industry, including Boxpark, Crêpeaffaire, Patisserie Valerie, and many others, totalling 660 operators across over 1400 sites.

Table of contents

What is Onvi?

Whether a café, local restaurant, or gourmet food truck, Onvi empowers ambitious hospitality businesses to start and expand their operations through a sleek, modern, and easy-to-use POS (Point Of Sale) system.

With Onvi's POS solution, customers can order and pay either in-person or through online ordering in just a few clicks without downloading an app, filling out a registration form or entering login information. Ultimately, Onvi delivers a seamless, uninterrupted customer journey; with the mission of becoming the single system of choice for SME operators.

Onvi POS with complete hardware setup
The hardware setup of the Onvi POS consists of the StarIO mC-Print2 receipt printer (left), the StarIO cash drawer (centre), the iPad running the Onvi Serve app (centre), and the Stripe WisePad 3 card reader (right).

I first joined Onvi in August 2020, a week after we had launched the first iteration of the base Mobile Order and Pay application. At the time, this was our only product, and during the over two years I was there, we would radically transform the product offering into a fully-fledged POS system to be used by over 1400 sites across the country, successfully process over 4 million orders, and take £66 million in GMV.

I thoroughly enjoyed being part of the team and helping the young company grow. As a crucial team member, I was a principal engineer on the three projects that made up Onvi's final product offering. These three parts, together, make up the all-in-one order and payment solution for hospitality businesses.

Onvi Order

Onvi Order is the mobile-optimised web application through which an operator's customers can order and pay for items from their table, via Apple Pay, Google Pay, or debit and credit card, without the friction of having to download an app or fill in registration details.

This web app was used by thousands of customers in bars, cafés and restaurants daily; it was an essential cash stream for our operators. Therefore, its stability, security, and speed were paramount.

To ensure stability, I enforced a rigorous testing regime. Every Vue component and utility I wrote were accompanied by comprehensive unit and integration tests using Jest, and all user flows were accompanied by suites of integration tests using Cypress. This included all bugs and issues that came to light, which were also heavily tested afterwards to avoid regressions.

Speed was also a significant consideration. Not only did I have to account for internet speed, but I also ensured that JavaScript execution was smooth and fast. This ranged from deferring the loading of scripts until they were required to delaying non-critical function calls to ensure smooth CSS transitions during DOM operations.

Crêpeaffaire using Onvi Order
Onvi Order's UI was made to be customisable to cater for operators' brand guidelines. A JSON is stored against each operator containing the brand's styles.

Boxpark using Onvi Order
Onvi Order was also designed to be used on a desktop, as well as mobile devices.

Onvi Control

Onvi Control is the central hub used by operators for managing their business. From here, operators can:

  • Create and manage their menus to be accessed by customers on Onvi Order and Onvi Serve through Menu Enrichment.
  • Manage availability for menus, categories, items and modifiers.
  • Monitor and update orders coming into their sites.
  • Download marketing assets (including QR codes customers can use to access menus on their phones through Onvi Order).
  • Deep-dive into Analytics and see what's driving their business performance.

Onvi Control was also built using Vue.js, with Jest and Cypress used for testing. Several features utilised a complex data structure, such as that of Menu Enrichment. We designed the data structure to be returned from the REST API to support many-to-many relationships, which resulted in the use of Vuex ORM (Object-Relational Mapping), which helped to normalise the data and significantly improve data storage and retrieval.

Enriching a menu in Onvi Control
A screenshot from Onvi Control. It shows the "Menus" page. From here, operators can create items, categories and menus to be shared and used in both Onvi Serve and Onvi Order.

To differentiate from other POS systems, we researched how to integrate analytics into Onvi Control. This would bring operators closer to their sales data in a simple, easy-to-consume way. From viewing daily sales performance to seeing which items are selling the best, our goal was to provide greater insight into how their business was doing.

Working together with our Data Scientist and Product Designer, I worked on integrating Sisense.js, a third-party data visualisation service. Since we had several pages for which we wanted to show charts and allow the operator to filter them by site, date range, sales channel, and more, I had to architect the implementation modularly. This would reduce the complexity of the integration, allowing for greater reliability and future scalability.

"Sales by date" Analytics in Onvi Control
Operators can get visibility on their sales over time. They can show their sales over the last seven days, last twenty-eight days, or they can select a custom range to filter by.

"Item sales" Analytics in Onvi Control
Operators can also view which of their items are best-sellers. They can also filter the chart by date, sales channel, fulfilment mode, and tag.

Onvi Serve

Onvi Serve is an app for iPad and iPhone that can be downloaded from the App Store. From here, operators can:

  • Take orders at the till, with either cash or card payment.
  • Manage live orders through the KDS (Kitchen Display System) across the table, pickup, and delivery channels. Quickly see, at a glance, all your orders and understand what action needs to be taken.
  • Set up printer routing. Print jobs only where they are needed, based on menu categories. For example, you can route customer receipts and snacks to the counter, hot food to the kitchen, and drinks to the bar.

When we first moved from just a Mobile Order and Pay application (Onvi Order) to building a fully-fledged POS, there were several things to consider. We knew we had to offer card payments through a till, and we knew we had to couple this with receipt printing so that customers could receive purchase receipts and the kitchen could print tickets to manage their orders. This would require Bluetooth and USB / Ethernet connectivity to connect card readers and printers to the app.

What is Capacitor.js?

After researching and testing several technologies, we agreed to proceed with Capacitor.js to build our native iOS app for iPhone and iPad. Limiting to iOS only could ensure a baseline quality user experience. We tested many Android devices, from Samsung to Google tablets, and as expected, we discovered that our prototype Capacitor.js app would run slower on one device than another. You would click on a button, and the device would take a second to respond. It was an inconsistent, jarring experience, which would have made optimising for all devices a time-consuming task – time which we did not have. And more importantly, users cannot wait for the device to respond whilst taking orders in the heat of service. Imagine a customer dictating their order to you whilst you're still adding the first item to the order due to how slow the device is. Sucks, right?

Using Capacitor.js also meant that we could use our existing architecture with Onvi Control. Components, ORM models and helper functions could all be shared across these two projects. Consequently, we created an NPM package called Onvi Core to share the common assets between both applications. This allowed us to develop significantly faster and ultimately deliver the Onvi Serve app to the market within only a few months.

How does it work?

If you're familiar with React Native, you'll understand the concept of bridging. Capacitor.js operates on the same principle, allowing bidirectional and asynchronous communication between the JavaScript layer and the Native layer (Java/Kotlin for Android and Objective-C/Swift for iOS). This is especially useful when we want to utilise native functionalities not directly accessible through JavaScript, such as connecting to external devices via USB or Bluetooth.

Let's take the example of discovering and displaying a list of available printers in our Onvi Serve app. We'll break it down into three components:

  1. StarPrinterPlugin.js

This component handles the interaction with the native code from the JavaScript layer. We register our plugin and call functions that correspond to definitions in StarPrinterPlugin.m.

StarPrinterPlugin.js
import { registerPlugin } from '@capacitor/core'

export const StarPrinter = registerPlugin('StarPrinter')

export class StarPrinterPlugin {
  ...

  /**
   * Finds and returns printers connected via the provided `searchTargets`.
   * @param {String[]} searchTargets The search targets to include. e.g. ['BT:', 'USB:', 'TCP:']
   * @returns {Promise}
   */
  discoverPrinters (searchTargets) {
    return StarPrinter.discoverPrinters({ searchTargets })
  }
}
  1. StarPrinterPlugin.m

Here, we register our native functions and inform Capacitor about which functions are accessible from the JavaScript side. Asynchronous communication is facilitated using CAPPluginReturnPromise to handle the back-and-forth between the two layers.

StarPrinterPlugin.m
#import <Capacitor/Capacitor.h>

CAP_PLUGIN(StarPrinterPlugin, "StarPrinter",
  CAP_PLUGIN_METHOD(discoverPrinters, CAPPluginReturnPromise);
)
  1. StarPrinterPlugin.swift

And finally, this component contains the native implementation. We register our StarPrinterPlugin with Capacitor and define the discoverPrinters function. To pass the response back to the JavaScript layer, we use call.resolve to resolve the promise with the desired data.

StarPrinterPlugin.swift
import Capacitor

@objc(StarPrinterPlugin)
public class StarPrinterPlugin: CAPPlugin {
  ...

  @objc func discoverPrinters (_ call: CAPPluginCall) {
    let searchTargets = call.getArray("searchTargets") as? [String] ?? []
    
    // Create printersJSON.
    ...

    call.resolve([ "printers": printersJSON ])
  }
}

For more in-depth information on how Capacitor.js plugins work, you can refer to their official documentation available here.

What does the user see?

From a user's POV, their experience using the Capacitor.js app is no different to using a native app. Therefore, with Onvi Serve, we had all the functionality of a native app, minus the learning curve, heavy upfront work, and maintenance required to build a fully native iOS app.

Let's consider the example of connecting the Stripe Wisepad 3 card reader to the iPad.

Onvi Serve
To connect a card reader, the operator navigates to Settings > Card reader > Add card reader. They are then taken through a connection wizard to help them connected with ease.
Onvi Serve
To connect the card reader to the iPad, the operator must enable Bluetooth and Location permissions. Stripe requires location permission to help prevent fraudulent payments. But as you can see, the permission dialogs appear as they would with any other native iOS app.
Onvi Serve
After the card reader is connected, the operator recieves a toast notification and can then optionally view additional information about the reader.
Onvi Serve
When paying for an order via card, the operator is presented with a 'Waiting for payment' screen whilst waiting for receipt of contactless card payment or chip and pin payment. After the payment is made, the operator is taken to a success screen where they can then optionally print or email receipts for the customer.

Delivering at velocity

Scalability roadblocks

In the first few months of my time at Onvi, we adopted an agile two-week sprint workflow. Although this allowed us to ship new features at a reasonable velocity, we encountered problems as our team, number of projects, and complexity grew. Some of these problems included:

  • Two weeks was not enough time to ship a feature, if ship anything at all. Tickets frequently overflowed into the next sprint, with no clear deliverables or delivery date.
  • After a sprint, we'd quickly jump into the next one with little time to assess, learn and improve our processes.
  • There was a disconnect between product and engineering. The product team would create and assign tickets just before the engineers would start to work on them. There was little collaboration between teams, with little time to scrutinise and assess ways forward. Ultimately, there was no joint ownership of the work.

To scale effectively, we needed to adopt a new approach to working. One that allowed us to re-focus on the product and customer needs and allowed us to not just guess when work would be finished but almost guarantee (with small tolerances) when it would end.

From Sprint to ShapeUp

Shape Up by Ryan Singer, printed edition

Whilst discussing ways forward, our Head of Product suggested we give Shape Up: Stop Running in Circles and Ship Work That Matters a read.

ShapeUp highlighted several of the issues we were experiencing, and after trialling and eventually adopting full-time, our internal operations had been overhauled to a remarkable degree. We ultimately increased our efficiency and released significant new features regularly whilst maintaining high-quality standards throughout the product and service offerings.

How does it work?

Using Basecamp's ShapeUp, we worked in six-week build cycles.

Six weeks is long enough to build something significant from start to finish whilst being brief enough for the team to sense the approaching deadline from the very beginning. Almost all of our major features were built and released in a single build cycle.

A fundamental principle is that projects do not get an extension if they exceed their deadline. We ensure this in the Shaping phase, where time estimates are taken out of focus in favour of the appetite. Instead of asking how much time it will take to do a piece of work, we ask: How much time do we want to spend?.

An appetite is entirely different from an estimate. Estimates start with a design and end with a number. Appetites begin with a number and end with a design.

Vitally, the project is handed to small, self-organising, integrated teams. Each "squad" generally consists of one frontend engineer (such as myself), one backend engineer, and one product designer. As a team, we are responsible for moving the project forward and ensuring its timely delivery. We define our own tasks, make adjustments to the scope, and work together from the offset to define, design and architect the solution. We are a team, and we are all responsible for each other's work.

This completely differs from other methodologies, where managers chop up the work and programmers act like ticket-takers.

I appreciate this has been a brief overview of our workflow at Onvi. Still, I would wholly recommend giving ShapeUp a read - it does a far better job than I have done here at explaining its benefits and how to use it effectively. Additionally, my former colleague, Chris Boakes, successfully introduced ShapeUp at Zoopla and shared his experience in an amazing blog post available here.

If you've experienced any of the problems above, I would definitely give ShapeUp a try; it may transform your operations as it did for us.

Delivering 'Printer Routing'

Before this feature, the operator could connect a single printer to their iPad, where all customer receipts and kitchen tickets would be printed. This was great for food trucks, but what if your setup was slightly more elaborate? Clients wanted a way to determine where specific menu items would print to. For example, you could set "Wine" to print to the bar, whereas "Main courses" could print to the kitchen. This would eliminate the need for kitchen staff to run to-and-fro a single printer and manually distribute tickets themselves, improving kitchen workflow and efficiency.

This pitch and several others were placed on the betting table and debated. As a team, we agreed to take on the project, and the squad was assigned.

Cross functional teams

Our squad for developing this feature consisted of:

  • 1x Product Designer
  • 2x Software Engineers:
    • 1x Frontend Engineer (me)
    • 1x Backend Engineer
  • 1x QA / Automation Engineer
  • 1x Data Scientist

Screenshots

Using Miro for collating questions to ask clients
As a squad, we listed all the unknowns we had individually, and then curated a list of questions our Product Designer would ask to collate answers for during research.
Using Miro for the breadboarding process
The "Breadboarding" phase. Here we used Miro to sketch and discuss the key components and connections of the Printer Routing feature without specifying a particular visual design. We then used a breadboard to create and define our scopes.
Using Miro for defining our scopes
During the "Scoping" phase, we defined various tasks into frontend, backend and design and grouped them into the appropriate scopes. Scopes become the language of the project and help us to communicate its overall status through the use of a hill chart.
Diagrams depicting the various possible printer to iPad configurations
Diagrams were drawn to help us understand the various printer to iPad configurations.
A basic flow diagram for understanding how the backend implementation would work
The is responsible for the development and delivery of the project. This means everyone is involved with the design phase, including when working on the backend implementation.
A diagram for understanding the user flows in Onvi Serve
Early diagrams were drawn using Miro to help us understand how the various user flows when connecting multiple printers.
Wireframes of the screens in Onvi Serve
Wireframing the User Interface allows the frontend team member to begin development on adding screens and updating the routing configuration early in the project.
The final Figma designs of the screens in Onvi Serve
The final designs created using Figma.
A hill chart with our Printer Routing scopes
Using a hill chart gave us a way to communicate the status of the project via its scopes with external team members easily and quickly.