Skip to main content

· 15 min read
Tyson Cadenhead

Introduction

At Vestaboard, we believe in the power of simplicity. It is at the core of our product, which is a display board with split-flaps that can transition to display messages or artwork meant to surprise and delight.

When I stepped into the role of Head of Software Engineering 3 years ago, it was my goal to infuse our engineering culture with that same simplicity that has captured and inspired our loyal customers. To that end, in early 2024, we began discussing the idea of merging our native iOS and Android applications and our web application into a single codebase.

This began a journey that spanned the better part of 18 months. The result was the launch of our new and improved application.

Pain Points

We already had very high-quality Swift and Kotlin applications along with a React web app that mirrored the features of native. The main issue was speed of delivery. Inevitably, one of the codebases always lagged behind the features of the others. We constantly had to make the decision to either launch a new feature on only some of our platforms or to hold up a release in order for iOS or Android to catch up.

Coming from a web application background, native development, even with modern languages like Swift and Kotlin, seemed to take more cycles than I felt they should. There are probably many reasons for that disparity, not least of which is the developer experience and tooling in XCode and Android Studio missing key developer experience features that React Native excels at.

Also, I wanted to empower my team to be able to work across the stack more confidently. Having been in positions where frontend and backend developers are different people or even teams, I knew the wasted time that often arises from an API change blocking frontend development. We were already building out a comprehensive federated GraphQL endpoint that anyone on our platform team could easily update. It made sense for the same person to be able to supply the data from the backend and render it on the frontend. Our team of 3 full-time developers was too small to not be full-stack.

Planning and Proof of Concept

I knew we were at a point where we needed to rethink our engineering practices. The next step was to decide how we would unify our codebase. React Native is obviously not the only game in town when it comes to shared native codebases.

I had previously led a team that unified our codebase with Flutter, which is interesting because instead of rendering native components like React Native, it treats the entire app as a sort of game engine and quickly draws onto the screen. The reason we ultimately chose React Native was because of the ubiquity of TypeScript and React. We would have less issue bringing in developers who could understand our code.

I began a proof of concept rendering some message cards from my board’s history and a few of the existing screens from Vestaboard+, our subscription service for Vestaboard automations. It was rudimentary, but it showed the first glimmers of promise. I showed it to our Senior Full-Stack Engineer, John, and Head of Product, Mark. We decided to try to build a few of the most difficult features in React Native and Expo. If we could build those features in a way that felt native, we all agreed that it would be a promising path forward.

John built out our native onboarding flow while I built out our composer that allows you to compose messages to send or draw them automatically. After a few weeks, we had working prototypes of many of our more complicated features and we knew that we could build anything we needed with our new stack.

Rebuilding the App

So we started rebuilding the entire app screen by screen, feature by feature.

There are two ways to do a rewrite with React Native; you can include React Native code inside an existing native application and update it as granularly as one component at a time, or you can go completely greenfield and start from scratch.

The obvious benefit of updating an existing app is that you can start releasing new features in weeks rather than months, or even years and only rewrite features as they are needed while still maintaining the existing functionality.

On the other hand, doing a greenfield rewrite allows React to manage the application state at the top of the app. It unlocks features like Expo file-based routing, better developer experience and probably an ultimately more cohesive application. We opted for a full rewrite. This is not a decision to be taken lightly, but having seen failures and successes of using React Native in the past, I knew that if we wanted to actually get to a place where our entire app was React Native, we had no choice but to go scorched earth on it.

Shortly after we began, we hired a third contributor to our React Native rewrite team, Alex. He began looking deeply into performance metrics to help us determine why we were dropping frames on long list renders.

Performance

The star of our application is a component called the “Message Card”. It is a preview of what a message is in the history as well as a preview of what a message would look like if you send it to your Vestaboard. We use this component extensively across the app inside long infinitely scrolling lists and it soon became clear that any long list with this component would give us significant performance issues. The component itself was rendering the message as a dynamic SVG along with several buttons for functionality on the message card, many of which were animated using the Lottie library. Optimizing the message card took multiple rounds of tweaks, but we knew that not getting it right would be an obvious giveaway that the app wasn’t truly native.

SVG vs. Custom Font

First, we replaced the SVG rendering with a custom font with emojis for the colored characters that can display on our bits. We saw small improvements from that change, but it still didn’t feel native.

Conditional Rendering Hoisting

Next, we had some conditional logic inside each message card that would display modals for specific message actions such as prompts when deleting a message from history, a modal to allow the user to pin a message to the Vestaboard device for a certain length of time, a prompt for sharing and more. Since these were included with every message card, the render time was much higher than if it were just rendering a simple component. We moved those components higher in the app hierarchy by wrapping message card views in a MessageCardProvider with context handlers to trigger the modals with the appropriate data passed in. Again, we saw huge performance improvements, but we knew we could do more.

FlashList vs. FlatList

Up until this point, we had been managing our lists with FlatList, which ships with React Native. Shopify, a huge proponent of React Native, had built “FlashList” which purported to be a better version of FlatList, especially for longer lists because of its use of recycler view. We swapped it out and saw immediate improvements, especially as we got further down our long lists of messages.

Native Components

Opting into Expo and React Native didn’t mean we had to abandon native components entirely. We could write our own Swift and Kotlin code as needed. For example, we had an existing native widget that displays the current Vestaboard message from our previous apps. We were able to use the majority of the existing code to build that feature, which React Native didn’t support out of the box.

The ability to include native Swift and Kotlin modules in our code is one of the biggest strengths of React Native. It allows us to drop down a level when we need to by giving us an escape hatch. In our case, the existing React Native and Expo modules covered almost all of our features, but we were never locked in to only use them.

Adding New Features

If there were one gray area of the way we approached our initial release of the React Native app, it would be adding new features that didn’t previously exist in the iOS and Android apps. Looking back, if we had resolved to copy the existing app screen for screen before adding new features, we could have released sooner and gotten better feedback as we rolled out each new feature iteratively. However, one of the main reasons we were migrating to React Native was because many features we had planned had been held up by cross-platform support. This was our chance to launch not only with a new framework, but with many new features that we were betting on our customers enjoying.

Message Feed

The new feature that required the biggest time investment was actually the feed of messages. On our original app, the main screen had a list of your message history, favorites and daily message picks from our staff. These were all capped at 3 messages. Pressing the “more” button would send you to a screen where we would list the rest. It turns out that managing a single screen of scrolling content is much less complex than our new feed experience, which allowed infinitely scrolling for various types of messages with a tab view.

Push Notifications

We had also been planning a notification section of our app and decided to go ahead and include it in the initial version of our app as well. Thanks to the Expo Notification primitives, sending push notifications was not a difficult task.

OmniSearch

We completely rethought our search functionality and moved search up to a tab in the app. Instead of searching through specific models, like message history or Vestaboard+ channels, we built a powerful omnisearch which required indexing all of our content in ElasticSearch. Here it should probably be mentioned that in tandem with creating a new React Native app, we were moving many of our backend processes away from a monolithic architecture to distinct services built for specific tasks. The message service was in charge of handling every type of message and the ability to search through them. The task of migrating millions of existing records from the monolith to the message service was arduous. I personally had a background task running for nearly two weeks that batch-processed 1,000 records at a time and dumped them into our new service.

Haptics and Animations

We sprinkled haptic feedback and lottie animations throughout the app to give more positive feedback on user interactions. On the message card component itself, there is a paper airplane icon you can press to send the message to your Vestaboard. When you press it, you immediately feel a haptic vibration and the airplane animates as if it is flying away.

Transitions

We also added many new settings in our application. One of the more requested features was the ability to change the way the Vestaboard transitions from one message to another. The “classic” transition means that all of the bits flip at the same time until they resolve on the message that was sent. We added options to gradually activate the columns from left to right, right to left and from the outside in. Not only did these new transitions create interesting displays, they made the sound of the bits flipping quieter, which many of our customers had requested after the classic transition startled their pets.

Without a doubt, we could have gotten to our initial release much quicker if we didn’t ship with these features. On the other hand, if we had attempted to build all of these features in Swift and Kotlin, I’m almost certain we would still be building them.

Rethinking How We Release Code

Before moving to React Native, we would try to build a new store release every month or two. Each release required a full manual end-to-end QA cycle. Our iOS and Android developers would have to manage feature branches and carefully merge in the branches that were ready to release across both platforms.

Using Expo, we were able to rethink how we release our updates. Our team has practiced trunk-based development and continuous integration across most of our stack, but until we migrated the app, we weren’t able to experience the same benefits on the frontend.

Since we launched our app, we are able to release OTA updates as soon as our code is merged and passes our CI/CD pipeline. For new features, we apply feature flags using LaunchDarkly. These features are QA’d and released by toggling the flag on for the rest of our customers. This has completely decoupled delivering code from releasing features.

Of course, this only works with non-native changes. If we need to add or upgrade a native dependency, our GitHub action kicks off a build upon merging the new code that we can test and release if it passes our quality standards. Fortunately, native changes are very rare compared to application features. Additionally, the surface of change when releasing new native features is much smaller, which makes testing easier.

Expo and React Native fundamentally shifted the way we could deploy code. We were able to ship faster, safer and more continuously.

Lessons Learned

We learned a lot during our React Native migration about scope, performance, libraries, and tooling.

Scope

As with any migration, there are things we could probably do better in the future. The first one is not about React Native specifically, but about how we approached the rewrite. While I think we made the right decision to approach this migration as a greenfield app rather than doing a slower migration into the existing app, I think we should have narrowed the initial scope to matching the original app screen for screen. This would have given us a good foundation to start with and put the new app in our customer’s hands much earlier. All told, it took our scrappy team of 3 developers about 18 months to launch our app from the day we started. If we had cut the scope to not include any new features, I’m confident we could have cut that time in half. On the flip side, we might still be building the new features that we sprinkled in as other priorities bubbled up.

Component Library

We prioritized building a branded component library early on. In one of the more risky choices of our stack, we went with Tamagui for the base of our component library. Tamagui is less battle hardened than many of the other styling libraries available for React Native, but it was built from the ground up for support across native and web. Having the base components allowed us to move more quickly, because we were able to stack them together as needed.

Performance

Another lesson we learned was to pay close attention to performance early and often. Coming from a React web background, the browser can be more forgiving than a fully native application. Avoiding heavy re-renders by memoizing and batching data became much more important. Using the right components, like FlashList for handling long never ending feeds of data was essential. Beyond that, we became familiar with the React Native performance monitoring tools which gave us insights into dropped frames and how and why they occurred. We learned the importance of not blocking the native thread, which would result in layout jankiness. Our newer features tend to have a more performance-first approach because of the lessons we learned pursuing that magical 60 frames per second.

Tooling

We also made sure that we were including our third-party libraries early to ensure that everything was compatible. We use Sentry for error reporting and Pendo for analytics and reporting. Having those dependencies in place gave us confidence that we could support everything the native app had supported.

Embracing the Expo Happy Path

We relied heavily into the Expo tooling for building images, signing releases, sending push notifications and managing environment variables. We found that the more we relied on the solid foundation Expo had laid, the less resistance we had to fight against.

Conclusion

We are on the other side of the migration now. While it took significant effort to unify our codebase with React Native, I feel very confident that we made the right decision. We are able to move much more quickly now. Since launching the React Native app, we’ve almost completed support for our new physical product, Vestaboard Note in the app. Vestaboard Note was the result of a pivot we made early this year. I am not certain that we would have been nimble enough to reach full support for Vestaboard Note before its release if not for our new application.

Our day to day developer experience is drastically improved. We build and release features quickly across the entire stack at a pace we wouldn’t have thought was possible two years ago.

The future is bright for Vestaboard software engineering thanks to some solid technology choices and a ton of hard work. If your team is considering a similar migration, I hope our experience gives you confidence that it’s not only possible, but in many cases, it’s very worth it.

· 6 min read
John Franke

Hey, I'm John Ottenlips Franke, a member of the platform team at Vestaboard. I'm excited to share my journey of preparing for the fall rock climbing season and how Vestaboard, along with GitHub Actions, played a crucial role in my successful off-wall training routine.

The Crux of Consistency

Last fall, I had some big rock climbing trips planned, including following my first multi-pitch on southern Utah sandstone near Zion National Park and leading classic Southeastern US routes like "Tarantella (5.10a)", "Christine (5.10a)", and "Pet Sematary (5.11a)" at the Wild and Scenic Obed. My goal was to climb the routes as cleanly as possible with minimal falls or breaks. I knew I needed to up my game with a consistent routine, having struggled on a few longer outdoor climbs earlier in Winter and Spring.

My previous attempts at training were all over the place - random and mostly social gym sessions weren't cutting it. I realized I needed a more focused and structured approach to give the upcoming season my best effort. However, planning a routine and sticking to it can be very challenging.

Vestaboard: A Visual Reminder for Success

Enter Vestaboard. With Vestaboard, I could experience an inspiring daily reminder for my full-body calistenic workout routine, also known as a WOD or workout of the day. And the best part? I could automate the schedule of these reminders and the exercise choice with a small TypeScript file and GitHub Actions, ensuring I stayed consistent and had a variety of climbing movements to practice. This project wasn't my only form of training, but it was helpful in keeping me on track off the wall.

Planning the WOD

A key factor was planning small sets and reps that were not too aggressive. A steady and measured approach has proved more effective than my prior sporadic and intense sessions. It is important to note that I am not a personal trainer, just a hobbyist climber with internet access, so please consult a trainer if you plan on making your own workout routine. I got most of my exercises from YouTube and climbing blogs. I experimented with many different movements of varying intensities and found a set of exercises I could stick with and build on.

Tying together Vestaboard and GitHub

If you follow our Subscription API documentation, you can make your subscription in our web application and get a subscription key, secret, and ID to display custom messages on your Vestaboard. GitHub Actions can store these secrets securely. In programming and climbing, it is import to practice safety to a high degree. Let's keep those secrets safe!

Climb On: GitHub Actions in Action

GitHub Actions and Vestaboard turned my climbing aspirations into a well-defined routine, giving me an easy to read and timely display for each workout and making the entire process efficient and effective. To create a GitHub Action that runs on a schedule, you need a YAML file in the workflows folder with your cron expression and the bash scripts you want to run. In the root of your project, you can add a file and folder structure like .github/workflows/WODaboard.yaml.

name: "WODaboard"
on:
schedule:
# UTC zone
# daily @ 8 am CST
- cron: "0 13 * * *"
# or run on dispatch
workflow_dispatch:

jobs:
wod:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: |
bun install
bun run index.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VB_SUB_ID: ${{ secrets.VB_SUB_ID }}
VB_SUB_KEY: ${{ secrets.VB_SUB_KEY }}
VB_SUB_SECRET: ${{ secrets.VB_SUB_SECRET }}

Since I had some slightly more complicated logic than just posting a static message to my Vestaboard, I used a Typescript function I could run with bun. Bun lets us run TypeScript files without having to compile them to JavaScript first.

Here is a simplified version of the code that sends plain text to my Vestaboard.

// index.ts
// full code available on GitHub https://github.com/jottenlips/wodaboard
const sendMessage = async (text: string) => {
if (process.env.VB_SUB_KEY && process.env.VB_SUB_SECRET) {
await fetch(`${API_URL}/subscriptions/${process.env.VB_SUB_ID}/message`, {
method: "POST",
headers: {
"X-Vestaboard-Api-Key": process.env.VB_SUB_KEY,
"X-Vestaboard-Api-Secret": process.env.VB_SUB_SECRET,
"Content-Type: application/json"
},
body: JSON.stringify({
text,
}),
});
}
};

sendMessage("Hello World!")

This function can let us post the content we want to Vestaboard via the Subscription we created earlier.

  • Be sure to use process.env so you don't accidently commit your secrets to source code.

Reaching Beyond Scheduled GitHub Action

Notice the additional field workflow_dispatch under on in the Action example above. This configuration lets us run the Action from GitHub whenever we want via the Actions tab on your repository. workflow_dispatch can be very helpful when developing an Action to run on command, like displaying the workout immediately.

GitHub Actions isn't just about workout reminders for me. I also found another valuable use – deployment notifications. Setting up Vestaboard to alert me when a new build deploys enhances my awareness of the development process. Integrating with Vestaboard is valuable on any GitHub project where you have automated deployments and want to know when they have succeeded without checking the Action status. To accomplish this, we use the push field so that when a change happens to the main branch, the Action will run.

For this GitHub Action, I used curl instead of the TypeScript code since I have very static content to publish to Vestaboard.

// .github/workflows/notify-deployment.yaml
name: "notify-deployment"
on:
push:
branches:
- main

jobs:
wod:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- run: |
curl -X POST -H "x-vestaboard-api-key: $VB_SUB_KEY" -H "x-vestaboard-api-secret: $VB_SUB_SECRET" -H "Content-Type: application/json" -d '{"text": "{64} New Wodaboard Deployment! {64}"}' https://subscriptions.vestaboard.com/subscriptions/$VB_SUB_ID/message
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VB_SUB_ID: ${{ secrets.VB_SUB_ID }}
VB_SUB_KEY: ${{ secrets.VB_SUB_KEY }}
VB_SUB_SECRET: ${{ secrets.VB_SUB_SECRET }}

Conclusion

Thanks to Vestaboard and GitHub Actions, I met my climbing goals. I was able to lead-climb all three of the Obed routes with minimal falls or breaks. I even flashed one of the climbs with no falls or resting on the rope. I also made it up the Utah multi-pitch smoothly. Repelling down the 5 vertical pitches was a lot scarier than going back up 😅. The combination of visual reminders, automation, and structured workouts propelled me to climb a full grade harder than the previous year. I even completed a few v6 boulders in the gym this year.

I hope you consider integrating the Vestaboard API into your upcoming projects, whether for staying on track with your goals or creating something delightful and inspiring.

Code Example

Wodaboard Source Code

WOD

🧗‍♂️

Utah

Obed

· 3 min read
Tyson Cadenhead

Milo is my oldest son. He is 12 years old and he is non-verbal. Since his autism diagnosis 10 years ago, we have tried just about every augmentative and alternative communication (AAC) iPad app available to help him to communicate with us.

A couple of months ago, I was setting up some buttons on TouchChat when I saw that there was an option to add a WebHook as a button action. Right away, my wheels started turning. I have a Vestaboard mounted on the wall in my upstairs home office. What if Milo could send me messages during the day from his "talker"?

I started by adding a few buttons with messages that Milo could send to our Vestaboard.

Once I started digging into the WebHook implementation in the TouchChat app, I realized that they did not support custom headers for the webhooks, so I knew I was going to need to spin up a simple service to send messages to my board with our Read/Write API

I created an extremely simple Ampt application on my personal account to make the requests to the Read/Write API. TouchChat could accept a JSON payload for the POST body. I set up my service to expect a hard-coded token using the Ampt params package and a text string with the message to be sent.

Here is the entire codebase for the Vestaboard TouchChat application:

import { http } from "@ampt/sdk";
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import fetch from "node-fetch";

const app = express();

app.use(cors());
app.use(bodyParser.json());

app.post("/", async (req, res) => {
if (req.body.token !== process.env.TOKEN) {
return res.status(401).send({
message: "Unauthorized",
});
}

await fetch("https://rw.vestaboard.com/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Vestaboard-Read-Write-Key": process.env.VESTABOARD_KEY,
},
body: JSON.stringify({
text: req.body.text,
}),
}).then((res) => res.json());

res.send({
success: true,
});
});

http.node.use(app);

Finally, I updated all of the button actions in the "Vestaboard" screen I created on TouchChat. I setup a WebHook with a custom message for each button.

In a short amount of time, I had a completely working prototype that allowed Milo to send me messages anytime. The idea is that we could add more messages over time as his vocabulary and needs expand. Adding new messages would not require an update to the Ampt service, because the message itself is coming from TouchChat.

Here is one of the messages that Milo can send to the Vestaboard:

I realize that this is a one-off use-case that may be a bit esoteric for most Vestaboard developers, but this basic concept of using the Read/Write API with other applications' WebHooks can apply pretty universally. If you want to dig in, the source code is hosted on GitHub

This example may not change the world, but it might change our family. Vestaboard is empowering Milo to communicate in a new way!

· 4 min read
Tyson Cadenhead

Ampt is a Serverless framework that inverts the infrastructure as code paradigm to an even more intuitive idea of infrastructure in code. This allows you to quickly create scalable applications without the overhead of provisioning resources.

As of today, Ampt is officially launching to the public. We have been using Ampt at Vestaboard for several months now to simplify our infrastructure and as the backbone for many of our new Vestaboard+ features. The launch of Ampt is an excellent opportunity to showcase how simple it is to schedule and automate Vestaboard messages on a cadence.

Get Started with Ampt

The first thing to do is sign up for your own Ampt account. From there, you just need to install the Ampt CLI by running:

npm i -g @ampt/cli
ampt

Initially, this will prompt you to log in to your Ampt account. After you have, you can select "Create new app" from the CLI and choose a starter template. For our purpose, we'll go with "TypeScript API (Express)." We can name the app whatever we want, so how about something like "vestaboard-custom-clock" and we're all set. Ampt will spin up a new application and even start up a developer sandbox for you that is a fully operational serverless project.

In the root of your newly created project, there will be an index.ts file. Go ahead and delete all the code in there and import the task from Ampt. We'll use that to schedule a task. To make sure it's working as expected, you can just drop in a console.log. Let's make a task that logs every 1 minute:

import { task } from "@ampt/sdk";

task("vestaboard-clock", async (event) => {
const timestamp = `${new Date().toDateString()} ${new Date().toLocaleTimeString()}`;
console.log(`It is ${timestamp}`);
}).every("1 minute");

This will log every 1 minute with something like:

It is Mon Sep 18 2023 1:34:16 PM

It is important to note that you can also use tasks with a cron expression or to run a task once at a specific time. If you want to explore more options, check out the Ampt documentation.

Send a Message to your Vestaboard

Now, we just need to send a message to your Vestaboard when the scheduled task is fired. The first step is to enable the Vestaboard Read/Write API in order to get your token.

Ampt has a dashboard at https://ampt.dev/ where you can query data, set environment variables, see production logs and more. Since the Read/Write API token is a secret, you don't want to store that in your code, but instead, you'll want to add it as an environment variable in the "parameters" tab of the Ampt dashboard. Let's call it VESTABOARD_TOKEN. You can access it in code using the params export:

import { task, params } from "@ampt/sdk";

task("vestaboard-clock", async (event) => {
const VESTABOARD_TOKEN = params("VESTABOARD_TOKEN");
const timestamp = `${new Date().toDateString()} ${new Date().toLocaleTimeString()}`;
console.log(`It is ${timestamp}`);
}).every("1 minute");

Now, we just need to install a fetch package:

npm i fetch@2 --save

and make a request to the Read/Write API with our token:

import { task, params } from "@ampt/sdk";

task("vestaboard-clock", async (event) => {
const VESTABOARD_TOKEN = params("VESTABOARD_TOKEN");
const timestamp = `${new Date().toDateString()} ${new Date().toLocaleTimeString()}`;

await fetch("https://rw.vestaboard.com/", {
body: JSON.stringify({ text: `It is ${timestamp}` }),
headers: {
"Content-Type": "application/json",
"X-Vestaboard-Read-Write-Key": VESTABOARD_TOKEN,
},
method: "POST",
});
}).every("1 minute");

That's it. With just a few lines of code, you've set up a scheduled task in Ampt that automates your Vestaboard. Imagine the possibilities!

If you'd like, you can clone our repo on GitHub with a full working example.

· 2 min read
Tyson Cadenhead

I came on full-time at Vestaboard about a year ago and started the process of discovering what we already had as well as taking part in building a vision for what the future of Vestaboard engineering will look like. Over the past year, we've built up our internal engineering team and focused heavily on new feature development as well as handling ongoing bug fixes and tech debt.

Our mantra has been to simplify the Vestaboard platform and to make it more scalable and resilient.

The truth is, our flagship product is very simple: we have a beautiful board that displays messages when you send them.

In the process of revamping our documentation, and in rebooting the Vestaboard Developer Community, we hope to bring back the fun and simplicity that makes Vestaboard so appealing as a software engineer in the first place.

You may notice that the new Subscriptions API is simpler than the old version. This is in part to stop exposing internal data structures that get in the way of quickly working with our APIs. It is also to dial things back and start to understand what endpoints provide value to you, the developer, so we can harden them and make them work better for everyone going forward.

In the coming weeks, we will be adding more content here. We will be creating quick-start guides to simplify the process of sending messages. We will provide solutions we've used and solutions we've seen in the wild that make building integrations and tasks that send messages easier.

If you haven't already, please join our Slack community to stay up to date on everything and we would absolutely love any feedback on how we can make the developer experience better.