Database-driven realtime architectures are becoming more and more common as evidenced by key backers and widespread use of software like Firebase and Supabase.
The two key priorities for an app following database-driven realtime messaging are long-term storage and change data capture (CDC) updates from the database.
In this two part article series, we'll take a detailed look at a fully serverless chat app where you can edit previously published messages. The chat app uses the Ably Postgres connector to achieve this, the details of which you'll see throughout the article. You'll find the architecture and the example app useful if you are looking for ways to build database-driven applications that work seamlessly at scale.
We've previously written about decoupling databases from realtime messaging:
The main problem is lack of modularity: each tool should do its own job. Databases are bases of data. They should do one thing and one thing well, which is to be the one base source of truth and data. They should never be saddled with realtime concerns --- that is not their strength by design. They should do storage, and a different dedicated piece should do realtime comms. All the other problems stem from this.
While the idea of a realtime database sounds great and opens up a huge range of possible use-cases one could build with this architecture, a tight coupling of databases and realtime messaging might suffer from various issues described in the article linked above.
Moreover, not all event triggers constitute consequential payloads and thus don't need to go into storage. Perhaps some events are just transient to make a client aware of an event occurring, not necessarily even descriptive details about that event. For example, in a chat app, I'd be interested in storing messages, timestamps, etc. but not necessarily typing indicator events.
In a recent article, I introduced the Ably-Postgres connector built by one of our community experts. It uses Postgres DB's listen/notify feature to listen for changes on any DB tables and publish updates on specific Ably channels whenever a change occurs. This allows us to take advantage of database-driven architectures without worrying about the scalability of realtime messaging or the awkward relationship between the two.
Check out the editable chat app: https://serverless-scalable-chat.netlify.app/
Let me present to you a complex looking architecture which will make more sense by the time you've worked through this article.
From an end-user perspective, they will be publishing messages on the frontend app and expect to receive messages on it as well. The same goes with editing any messages: all participants will need a way to edit their own messages and also receive updates about any messages edited by others.
A common architectural set-up when using a pub/sub messaging service like Ably is to publish updates on a channel to which the client is also subscribed. Although this works perfectly well for regular chat messages or any other event triggers, it is more complex to edit previously published messages or to trigger updates about changes to previous messages because Ably messages are immutable by nature.
It is, however, possible to implement this functionality by using a slightly non-traditional approach. Instead of subscribing to a single chat channel to which users are publishing their messages, we can separate out the incoming and outgoing chat channels. Doing this allows us to perform various operations on the data before it comes back in a subscription callback. A common use case of this architecture is message filtering like applying a profanity filter.
In the case of the current chat app, we'll make use of a database to store all the published messages directly in a table. We'll also have a listener that can
i) observe the
delete changes in the chat data table of our database, and
ii) publish a message on an Ably channel with the name of the operation as well as with the change data capture (CDC).
If we make our front-end clients subscribe to this channel into which the listener is publishing database updates, we'll not only receive new messages as a result of
insert operations in the database, but also updates on previous messages resulting from
update operations on the database. Each Ably message comes with a unique
msgId assigned by Ably, so we can make use of this to uniquely identify each message in the table. The database will be the single source of truth in the app and also useful if we'd like to load previous messages in the chat like in the Ably-Airtable starter kit example.
Before proceeding, take another look at the architecture diagram above to put all the steps in perspective and tie it all together.
We have four main goals with the editable chat app:
- Serverless architecture
- Editability of messages
- Storage of messages
In view of the above, let me explain some of the reasoning behind various tech choices in this chat app, along with some alternative options.
Nuxt aims to provide best-practice solutions to common web development problems like routing, state-management, code splitting, etc. It allows us to make use of various NPM utility libraries in a static site that can be deployed and used directly from a CDN, without needing a server, i.e. following the Jamstack architecture.
In the case of our chat app, it is useful in terms of separating out state management entirely from the visual components, so developers of all tech stacks can understand the communication and data exchange between the chat app and external services (mainly Ably in this case).
You can replace Nuxt with any other front-end web framework, vanilla JS or even use a native mobile programming language, depending on the needs and wants of your app.
Ably is a realtime messaging infrastructure as a service. It allows you to enable publish/subscribe-based messaging in your application with just a few lines of code. Ably provides highly-reliable low-latency messaging, and is able to work globally on any platform or device. It completely abstracts away the complex problem of scaling realtime communications across multiple regions around the planet, so developers can focus on their app logic.
We use PostgresDB to store messages from the chat app. In general, any database transactions which change table data shouldn't be done directly from the front-end to avoid potential security risks. Hence, we'll make use of AWS Lambda functions to make changes to the database on the users' behalf. Given that we are aiming to make this app fully serverless, Lambda functions fit right in with this theme.
Postgres is an open-sourced SQL database. Its performance and reliability make it a good choice for complex production applications. There's another special reason to choose Postgres as you'll see in the next point.
Postgres doesn't come with hosting, we'll need to make use of another service to host the database. Again, in light of keeping everything serverless, I've made use of AWS RDS for Postgres. Using AWS also gives the advantage of the accessibility of the data between other AWS services, like the Lambda function in the previous point.
Ably Postgres connector to watch changes on the database tables and publish messages on every change
One of the key requirements of this chat app is being able to listen to changes on the database tables and publish these changes to Ably. The Ably Postgres connector is a community-built project which makes this possible. We use Postgres because the built-in listen/notify feature makes this connector work. We'll take a detailed look at it later.
AWS Fargate is a serverless compute engine that hosts containers. The Ably Postgres connector has a dockerized image that needs to be hosted somewhere. We'll use AWS Fargate to do this, because it makes it easy and secure to manage the backend deployment and hosting on a single service like AWS.
AWS Fargate works with AWS ECS which enables deployment and management of containerized applications. We use AWS ECR to upload the docker image so it can be stored in the registry to be used by ECS as needed.
Netlify provides a serverless platform to deploy web applications. It also allows setting up git-based workflows to automate building and deploying new versions of a static site as changes are made to the repository. The Nuxt app is deployed using Netlify.
Netlify's serverless platform also provides serverless functions which can be invoked to perform a piece of functionality. The Ably service requires clients to be authenticated in one of the two ways: basic authentication or token authentication. Basic authentication exposes the API Key directly in the frontend script, and thus shouldn't be used in production. You should almost always choose Token authentication. To enable this, we need to set up an authentication endpoint that can verify the credentials of the frontend client and issue Ably Token Requests. The frontend client can then use this Ably Token Request to authenticate with Ably and use its service.
Given that we use Netlify to host the chat app, it's only natural that we make use of Netlify functions to host our authentication endpoint. Even though AWS Lambda is already a part of the tech stack, it would require us to set up an IAM for our users before they can access AWS Lambda. Netlify, meanwhile, makes it very easy.
Before moving on to the details of the chat app, let's first understand the working of the Ably Postgres connector that makes this architecture possible.
I recently wrote an article explaining the Ably Postgres connector in detail:
The connector accepts a configuration file where you input the connection details for your database as well as for the tables you want to listen to for data changes. It also accepts an Ably API key to be able to instantiate and publish messages to your Ably app following any changes to the specified tables.
Using the config file, the connector creates in your database a special table called the "ablycontroltable". This table is used to maintain the Ably channel mapping for different changes to the tables in your database.
Next, the connector creates a procedure to listen to changes on the specified tables using the
pg_notify function. This notify function then publishes the change data capture (CDC) payload on the relevant Ably channel, as specified in the config.
With this, I hope you have a better understanding of the high-level architecture of the serverless editable chat app.
In the next part of this two-part series, we'll take a closer look at various components of the chat app and dive into some code to better understand how each step is implemented.
Here are a few things we'll see in the next part:
- Navigating the Nuxt app (even if you are not a Nuxt developer)
- VueX state management
- Postgres DB setup
- Lambda functions setup on the Ably Integrations dashboard
- Ably Postgres connector setup
- Deployment of all the components
Feel free to reach out to me if you have any questions.