Building & monetizing Miro apps

Vedran
Miro Engineering
Published in
9 min readSep 28, 2022

Hello, I’m Vedran! 👋 I’m a full stack engineer from Canada, Cofounder of Blossom and over the past 4 years, I’ve built dozens of apps and integrations for platforms like Greenhouse, Figma, FigJam, Coda, Zoom, Slack, and Miro. Most recently, I built the Super Search app for Miro. Check it out if you find yourself needing to search and replace content on your boards.

In this article, I’d like to cover why I chose to build on Miro’s developer platform and share a couple tips to get you started developing your own apps. I’ll focus on how you can:

  • Share your app with users ASAP
  • Skip the backend
  • Use Miro’s built-in authentication
  • Use Miro’s CSS library
  • Maintain a local development environment
  • Keep your billing system simple

So, why Miro?

Simply put, here are some of the reasons I’m betting on Miro:

  • Over 600% user growth from 2020 to 2022
  • Company strategy is heavily focused on integrations
  • Their platform is still relatively new and not yet saturated with apps. Imagine building an iPhone app during the launch of the App Store. That’s the opportunity you have today with Miro.
  • Great support through Miro’s Developer Community on Discord

As you may have heard, Miro is also running a Community App Contest until October 14. They’ve even offered a few ideas on what to build if you need some. Take a look and join their new developer platform — there’s 40 million Miro users who could be using it. I’m participating in the contest myself, and I think you should join too!

During the contest, I’ve been asking the Miro Developer Relations team a ton of questions in their Discord and they’ve been incredibly helpful with their guidance and are always eager to answer questions, brainstorm, and help me navigate any issues I’ve run into. Big shout out to Anthony Roux and Will Bishop! If you’re building on the Miro platform, definitely join the Discord and say hi.

Start with a simple template in just 2 steps

For this post, I’ll focus on apps that use the Miro Web SDK, but Miro also has a REST API and Live Embeds that are definitely worth checking out.

This post also assumes that you’ve read the Miro Web SDK hello world example and that you understand how the Miro Web SDK works.

Step 1

I start with the Create React App template:

npx create-react-app my-app-name --template typescript

Then I modify public/index.html to include the Miro Web SDK script:

<!DOCTYPE html>
<html lang="en">
<head>
[...]
<script src="https://miro.com/app/static/sdk/v2/miro.js"></script>
<title>Your app title</title>
</head>
<body>
[...]
</body>
</html>

Step 2

Then I use React Router to cleanly separate the code that runs in the headless iframe to initialize the app from the code that powers the user-visible panel.

// src/index.js
import { BrowserRouter, Routes, Route } from "react-router-dom"
import * as ReactDOM from "react-dom"
import Panel from "./Panel"
import Headless from "./Headless"
const App = () => (
<div>
<Routes>
<Route path="/headless/*" element={<Headless />} />
<Route path="/panel/*" element={<Panel />} />
</Routes>
</div>
)
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
)
// src/Headless.tsx
import { useEffect } from "react"
const Headless = () => {
useEffect(() => {
window.miro.board.ui.on("icon:click", async () => {
await window.miro.board.ui.openPanel({
url: `/panel`,
height: 600,
})
})
}, [])
return null
}
export default Headless
const Panel = () => <div>Hello!</div>
export default Panel

Now you should have a simple Miro Web SDK app with an icon in the sidebar that will open a panel that says “Hello!” when clicked.

Note: You may need to look under “More tools” to find your app.

Bonus: Use Miro’s official create-miro-app template to get started even quicker. I created my own template before discovering this official one, but it looks like a great option.

Share your app with potential users as soon as possible

Many platforms require you to publish apps, share code directly, or restrict unpublished app sharing to your team only.

Miro allows you to share your unpublished app with any Miro user. Take advantage of this amazing feature by sharing your app early and often. Find your App Settings and scroll down to the “Share app” section to copy a shareable link.

Share this link to people you consider early adopters who will give direct and honest feedback to improve your app.

Note: Before sharing the link, make sure your app is deployed to a domain that other users can access. If your app is only running on localhost, you can use a tool like ngrok to generate a publicly accessible URL that serves your locally running app. Be sure to update your App URL to point to this domain as well.

Don’t build a backend unless you need it

A Miro Web SDK app is just an iframed webpage that uses an SDK to communicate with the current Miro board. This means you can serve a Miro app from any of the popular platforms for hosting static sites. I use Netlify to deploy the frontend of my apps because it is free and provides a publicly accessible URL I can use to host my Miro app directly. Netlify hosting also satisfies the TLS and HSTS requirements from Miro’s App security guidelines out of the box.

Really think about whether you can launch the first version of your app without a backend.

Typically you’ll need a backend to store data permanently or to restrict access to sensitive content like user data, API credentials, databases, etc. If your app doesn’t have these requirements, but still needs to store some data, consider using Miro Web SDK functions like setAppData and getAppData to save data into the board directly. You can also use App Cards to store information, or even your browser localStorage, depending on your use case.

If you do need a backend, don’t start with a complex architecture that scales to millions of users. Embrace some tech debt (for now) and use technologies you’re comfortable with. Personally I use Digital Ocean’s App Platform because I find it simpler to manage than AWS, but I’ve also heard good things about Netlify.

Use the authentication feature included with the Miro Web SDK

If your app needs authentication, typically that means you need to support:

  • Signing in
  • Signing out
  • Signing up
  • The app experience for both signed in and non-signed-in users
  • Authenticating with username and password, a magic link, or something like “Sign in with Google”
  • “Forgot my password” flows
  • Database to store the user’s details and their session

I recommend you skip all of that by using the authentication built into the Miro SDK.

The JWT-based authentication from the board.getIdToken() function will generate a secure token that represents the current user’s session. You can send this token to your backend with every API request and use the client secret to verify the request.

Here’s a Typescript example for making authenticated requests using miro.board.getIdToken():

// A helper function to make authenticated requests to your backend endpoints
export const fetchAPI = async (endpoint: string, params: any) => {
const miroToken:string = await window.miro.board.getIdToken()
const resp = await fetch(`${API_BASE_URL}${endpoint}`, {
...params,
headers: {
authorization: `Bearer ${miroToken}`,
}
}
if (!resp.ok) {
throw new APIError(resp)
}
return resp
}

Here’s a sample backend python function that uses the popular PyJWT library to validate the authenticated request and returns the Miro user ID.

from django.conf import settings
import jwt
def get_authenticated_miro_id(request):
AUTH_ERROR = 'Authentication failed'
auth_header = request.META.get('HTTP_AUTHORIZATION')
if not auth_header:
raise Exception(AUTH_ERROR)
# Extract the token from the authorization header, which is in the format "Bearer <token>".
auth_split = auth_header.split(' ')
if len(auth_split) != 2:
raise Exception(AUTH_ERROR)
# Use the JWT library to simultaneously decode and verify the token was signed with your app secret
auth_data = jwt.decode(auth_split[1], key=settings.MIRO_CLIENT_SECRET, algorithms=['HS256'])
if auth_data['iss'] != 'miro':
raise Exception(AUTH_ERROR)
miro_user_id = auth_data['user']:
if not miro_user_id:
raise Exception(AUTH_ERROR)

return miro_user_id
import jwtdef get_authenticated_miro_id(request):AUTH_ERROR = ‘Authentication failed’auth_header = request.META.get(‘HTTP_AUTHORIZATION’)if not auth_header:raise Exception(AUTH_ERROR)# Extract the token from the authorization header, which is in the format “Bearer <token>”.auth_split = auth_header.split(‘ ‘)if len(auth_split) != 2:raise Exception(AUTH_ERROR)

Use Miro’s official CSS library

The Mirotone CSS library has been invaluable for quickly building components that meet Miro’s design guidelines. Even though there is no native React support, you can use the provided class names to create tabs, buttons, forms, icons, and few other things.

Maintain a functioning local development environment

As you begin iterating on your app with real user feedback, you’ll find it helpful to maintain a local development environment that allows you to test bug fixes and build new features without affecting the production version of the app.

To do this, I recommend you create two versions of your app in Miro. One always pointing at your local environment, and one pointing to the production URL.

Keep your billing system simple

I get it. Building a billing system is not easy. Users need to be able to make purchases, change and cancel plans, use promo codes, and enter payment method details.

I recommend that you break down each billing requirement into individual pieces and build them one by one as needed. For example, if your app includes a 7-day trial, they don’t actually need a payment system for at least 7 days. Instead, take this time to discover active users by learning why they decided to install your app and whether it meets their expectations.

Even when you implement payment support, consider the number of active users you expect to have and whether you actually need a fully automated billing system. Consider whether you can support cancellation and plan change requests through a simple form that just emails you the request for you to manually update in your payment gateway (Stripe, Braintree, etc).

In terms of the technology to use to support purchases, I’m a fan of the Stripe ecosystem. In addition to their rich API, they also offer no-code/low-code options for both one-time purchases and subscriptions.

If you’re using Stripe Payment Links or Stripe Checkout, make sure to set a client_reference_id URL parameter with the value of a unique identifier for the current user so that you can connect the successful purchase to the correct user.

Here’s a Django example of how you can implement a webhook endpoint that will receive the checkout.session.completed event from Stripe when a user has completed a purchase using Stripe Payment Links or Stripe Checkout:

class StripeWebhookView(GenericAPIView):
permission_classes = [AllowAny]
def post(self, request):
"""
Stripe webhook requests
"""
# Check stripe signature
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
payload = request.body
event = None
try:
event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_WEBHOOK_SECRET)
except ValueError as e:
# Invalid payload
print("Invalid payload")
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
print("Invalid signature")
return HttpResponse(status=400)
event_type = event.type
if event_type == 'checkout.session.completed':
checkout_session = event.data.object
user_uuid = checkout_session.client_reference_id
user = User.objects.get(uuid=user_uuid)
user.purchased = True
user.save()
return Response()

The one lesson I wished I learned earlier

Some of my apps weren’t as successful as I had hoped, while others had steady growth and actually led to acquisitions. All of the apps seemed like great ideas at the time — at least to me! — and I felt they were worth pursuing.

So what was the difference between the successes and the failures?

The successes came from getting the apps into the hands of real users very quickly, and iterating continuously with them to create the best experience possible. User feedback and user research is the best validation for your applications. I learned that the longer I delayed putting the app into the hands of real users, the more likely it was to fail.

In that time, I learned a lot about building apps quickly on growing platforms like Miro and I wanted to share some of my lessons, hoping they will help you with your own projects.

Conclusion

I hope these tips help you build your Miro apps faster. I applied most of these tips when building my Super Search app for Miro. I’m also working on a few more posts on this topic, so if you’re interested check out my personal website or reach out to me at vedran@hookshot.ca. I’d love to chat.

--

--