Publish PostgresSQL Data Changes to React with KsqlDB and MQTT
Tags: mqtt, ksqldb, kafkaconnect, nextjs
Published: 2024-03-21 23:03:49

The cover image illustrates the aim of this post. First, upon executing an update SQL in the backend PostgresSQL database, a table cell in the React frontend was almost immediately updated and highlighted, showing the new value provided in the update SQL. The data streaming flow was composed using Kafka Connect, KsqlDB and HiveMQ MQTT broker, with all source code is available in this [github repo](https://github.com/kwonghung-YIP/publish-msg-with-kafka-connect-mqtt-nextjs).


![Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oy8as7kwj5jtoqcwq6mo.jpg)

## Architecture

- All components in the architecture diagram are defined in the [docker-compose.yml](https://github.com/kwonghung-YIP/publish-msg-with-kafka-connect-mqtt-nextjs/blob/main/docker-compose.yml) file. The setup aims to capture the data changes in PostgresSQL database and publish it into the React frontend. To mininize the delay, the client does not take the long polling approach for fetching the data, instead, the back-end aggresively pushes the messages to client via the MQTT v5 protocol, whenever a DB changes is detected.

- Another considertion is to utilitze product feature as much as possible, and reduce development effort. Apart from Next.js routing and React frontend, the back-end implementation only invokes PostgresSQL and ksqlDB scripts.

- With the *"cp-server:7.5.3"* docker image, the Kafka broker can be run in [KRaft](https://developer.confluent.io/learn/kraft/) mode, no longer depending on ZooKeeper.

- During Kafka Connect setup, the [confluent hub client](https://docs.confluent.io/kafka-connectors/confluent-hub/client.html#confluent-hub-client) was used to install the connectors and SMT transfomers. To setup the JDBC source connector, the PostgresSQL JDBC driver must be download into the docker image.


![Data stream flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uubdd8w9lu5oftnz0ee8.jpg)

## PostgresSQL

- The flow starts from PostgresSQL database on left hand side of data stream flow diagram. The *"horse"*, *"jockey"*, and *"race"* tables persist the basic entities, linked by the *"race_horse_jockey"* table. The *"v_race_horse"* view compose all four tables as a comprehensive view. Finally, the *"odds_forecast"* table persist odds data, and changes to this table are published to the React frontend.

- All tables, view, procedures and data initiation are defined in the [create-table.sql](https://github.com/kwonghung-YIP/publish-msg-with-kafka-connect-mqtt-nextjs/blob/main/postgres-db/init-db/create-tables.sql), executed when starting the Postgres docker image.

- The JDBC source connector captures the changes from the v_race_horse view, race and odds_forecast table, and publishing them to corresponding *"postgres_src_<table_or_view_name>"* Kafka topics.

## KsqlDB and Kafka Connect

- The different between [KTable](https://docs.ksqldb.io/en/latest/concepts/tables/) and [KStream](https://docs.ksqldb.io/en/latest/concepts/streams/) is when both receiving multiple messages with the same message key, KTable keeps the latest message only, but KStream keeps the entire history of the stream. Based on the data feature, *"v_race_horse"* view and *"race"* table are imported as a KTable, and *"odds_forecast"* table is imported as a KStream to capture every odds changes being published.

- The odds KStream, race_horse_tbl and race KTables were created with [CREATE STREAM](https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-reference/create-stream/)/[CREATE TABLE](https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-reference/create-table/) command. The command requires a source Kafka topic as a parameter, those KStream/KTable are serve as the input to KsqlDB.

- On the other hand, the [CREATE STREAM AS SELECT](https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-reference/create-stream-as-select/)/[CREATE TABLE AS SELECT](https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-reference/create-table-as-select/) command create KStream/KTable (KStream/KTable-query in Data stream flow diagram) as the output of KsqlDB, the new Kafka topics associate with the new KStream/KTable were the source topic of the Redis and MQTT sink connector.

- KsqlDB also provide the [CREATE CONNECTOR](https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-reference/create-connector/) command to define a new Kafka Connect connector. It is not necessary to install KsqlDB if you are only interest in using the connectors, you can skip KsqlDB by using the REST API provided by Kafka Connect also provides a REST API to manage the connectors. However, for convenience, all KStream, KTable, and connector creation were defined in the [init.sql](https://github.com/kwonghung-YIP/publish-msg-with-kafka-connect-mqtt-nextjs/blob/main/ksqldb/init.sql), executed after the docker-compose.yml is started.

## [RedisJSON](https://redis.io/docs/data-types/json/) and [RedisSearch](https://redis.com/modules/redis-search/)

- There are two Redis sink connectors using to publish data: “my-redis-sink-json” publishes the horse list of each race in JSON format, and “my-redis-sink-avro” publishes all odds in AVRO format. Data published into Redis are stored as an key-pair value, the key format is *"<topic_name>:<message_key>"*, and the value format is native JSON supported by RedisJSON. You can refer to [json.set](https://redis.io/commands/json.set/) command for how to create and update JSON value in Redis.

- RedisSearch support indexing and searching native JSON in Redis, the [ft.create](https://redis.io/commands/ft.create/) command is for creating *"horse"* and *"odds"* indexes for searching later.

## Next.js

- Route Handler of Next.js using to serve as the API backend to query the horse list and entire odds table by race date and race no. The API searches native JSON by [ft.search](https://redis.io/commands/ft.search/) command, horse an odds indexes.

- Next.js also hosts the React client components, with the data fetching feature, it calls the API and inject the horse list and odds table into the client component, it also allows to control the caching of the API calls.

## MQTT

- The *"odds_forecast_<race_date>_<race_no>"* KStream-queries read odds for a particular race from *"odds_forecast"* KStream and publish them into corresponding *"odds-forecast-<race_date>-<race_no>"* topics. The MQTT sink connector then exports odds to corresponding MQTT topic in HiveMQ. Due to limitations of the MQTT sink connector, it cannot exporting to MQTT topic dynamically based on properties in message payload. It is also not allowed to use [MQTT hierarchy topic](https://www.hivemq.com/blog/mqtt-essentials-part-5-mqtt-topics-best-practices/) because the Kafka topic name is mapped to MQTT topic directly, but slash “/” character are invalid to use in a Kafka topic name. KStream plus Spring Integration could an alternative to address these limitation, which can also customize the retention period based on the data type.

- The React frontend only shows one odds table of a particular race. The client instance created from MQTT.js library is kept in the [useContext()](https://react.dev/reference/react/useContext) hook and available globally. The client only subscribes to a single MQTT topic, reducing the unnecessary traffic pushing to client. With the [useEffect()](https://react.dev/reference/react/useEffect) hook, the component responds to unsubscribe the topic for housekeeping the connection in HiveMQ server.

## React frontend

- Finally, on the right-hand side of data stream flow diagram, which is the React web front-end. All odds data and changes must be reflected in the React state to be presented to user.


![Odds table in React frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6lbjxn1zcu5f8uj01tl.jpg)

- The URL *"…/odds/<race_date>/<race_no>"* determines the odds table being displayed, the parameters are captured by Next.js [Dynamic Routes](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). The horse list and entire odds table are injected into the React component by Next.js [Data Fetching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating) feature.
After rendering the odds table, the MQTT client subscribes to the corresponding MQTT topic. Whenever an odds message is received, it updates the React state, and the corresponding cell is highlighted in yellow.

## Further Enhancements

- Deploy the infrastructure to Kubernetes, possible with multi-nodes in different layers (Kafka, redis, HiveMQ).

- Replace MQTT sink connector with KStream + Spring Integration. Support dynamic topic mapping, topic hierarchy, and customize retention period.

- Replace JDBC source connector with Debezium connector.

- Replace MQTT message push with HTTP2 streaming, but I could not find the HTTP2 Sink connector.

- Enable OAuth authentication in different levels.