Hey, we're Spot! 🔵

Over the past few months we've seen how small businesses have struggled. Taking card payments is a must; but even the best card readers come at an upfront cost, and shipping can often be delayed.

We want to remove the barriers to getting paid, and getting your business back on its feet.

For Spot, all you need is a phone...📱

You (the business) enter the amount; we make a code for you to get paid.

Your customer scans the code and taps “pay now”. No logins, no typing – just the one-tap payment they're used to on the web, for all modern smartphones.

Click to authenticate – and you're done!

Using Google's realtime database, both of you see confirmation at the same time. And thanks to their Cloud Functions, your customer gets a receipt via email, too! 📬

By securely verifying each message we get, we make sure that if you see that tick, you've been paid 💰

How we built it

Spot is built with Google Cloud managed services at its core. We have split our application into a number of core components, each using different products to get running:

Our web payments gateway runs in Google cloud run, as a Dockerised Caddy + Node/Express microservice. This handles Stripe payments

Our email sending service runs as a Google firebase cloud function and talks to SendGrid, a transactional email API - No need for always-on infrastructure here.

Our real-time transaction database is a Google firebase realtime database - The backend and frontend can both interact with this, and it eliminates the need for managing websockets and live connections. That's one more server not needed 👏👏

Our React web app is hosted using Google Firebase hosting, so you can quickly access it anywhere in the world, thanks to their global CDN.

One of the core experiences of our app, is that customers can make payments using native Google Pay and Apple Pay support on their phone. This is possible thanks to a relatively new open standard called Web Payments. This is an API that is supported by most modern web browsers. This also works on desktop Chrome and Safari, and lets customers pay with card details that are stored alongside their Google or Apple accounts.

Here's our Google Cloud architecture diagram: Here's our Google Cloud architecture diagram

Here's a list of the technologies that we used for this project:


  • Google Firebase cloud functions - Serverless functions
  • Google Firebase realtime database - Managed database
  • Google Cloud Run - Managed container deployment system
  • Google API Gateway - Secure gateway to other Google Cloud infrastructure

APIs / Third party services

  • Stripe - Payments API
  • SendGrid - Transactional email API


  • Docker - Containerising applications
  • Node - Javascript, but for servers
  • React - Javascript framework for reactive web apps
  • ES6 - Shared between our Node and React applications
  • Styled components / SCSS - A really sweet way to create modular components, with fancy styles + This adds a lot of power to native CSS

Challenges we ran into

Making sure our application was secure was a big undertaking. We generate transactions in our backend, and encode our request to the customer as a QR code. When the customer scans this, they can make a payment. The only way for this payment to be successful, is if the amount they have paid matches the amount we requested!

Accomplishments that we're proud of

We hadn't used Google cloud before this project – we're proud of overcoming the difficulties with picking up a ton of different domain knowledge around serverless Google cloud functions & the other offerings that we made use of.

What we learned

We learned an absolute boat load of new technologies during this project, a special mention goes out to these.


I learned a ton about transactional email during this project. I haven't integrated with an email sending API before, and so I'm excited to take the knowledge I gained of SendGrid (+ A failed attempt at using Postmark) with me into my next projects. Being able to communicate with users through a medium that they're familiar with is hyper-valuable, and even more so with nicely designed email templates.


Before this project I hadn't worked with Google Cloud much at all, but I was hugely impressed with the Realtime Database – super easy to set up and get/set synchronised values across devices, which is no small feat! I'm excited to use more of the Firebase suite to build webapps in future.

I also enjoyed the chance to try out some After Effects and Audition to produce the demo video!

What's next for Spot

We would like to polish up our application ✨ and start sharing it with local businesses. Ultimately this project is all about making it easier for businesses to settle into our new normal, and we think Spot can help them get there.

There are some technical chunks of work that we'll need to tackle first, such as adding support for businesses to schedule their withdrawals. We think this could really be an asset to helping get businesses back on their feet.

Overall we think we'll make a go of this project, and put it in the hands of real users; we've definitely validated the concept thanks to HackTheMidlands!

If you want to follow along (shameless plug), you can find Bede Kelly online at @bedekelly and Aran Long at AranScope on Twitter 💬

One last thing: we haven't yet open sourced the project, as there are a few security bits we'll need to wrap up before we do. But we'll update the project with our open source GitHub repository when they're all done! The project will be found at (currently private) 🔜

Until next time 👋

Share this project:


posted an update

Here's a beaut that I learned along the way - A really nice library called 'Yup' that can be used for Express parameter validation.

exports.sendReceiptEmail = functions.https.onRequest((request, response) => {
  const schema = yup.object().shape({
    to: yup.string().email().required(),
    customer_name: yup.string().required(),
    merchant_name: yup.string().required(),
    card_brand: yup.string().required(),
    card_last_four: yup.string().required(),
    description: yup.string().required(),
    total: yup.string().required()

  schema.validate(request.body).catch(err => {
    return response.send(err, 400)
  // More code after here

Log in or sign up for Devpost to join the conversation.