June 23, 2022

Building a Digital Signage System using HarperDB

Welcome to Community Posts
Click below to read the full article.
Summary of What to Expect
Table of Contents

Digital signs have become a more common sight in everyday life, and I’ve often wondered: how could I create my own digital signage system? I also thought this would be a good opportunity to put a new product I’ve recently discovered called HarperDB to the test. Currently, I run my own containerized web cluster on my internal home lab set up to handle my smart home setup so I would love to replace it with a simplified setup, which HarperDB could provide.

What is HarperDB?

HarperDB, in simple terms, is a product that allows you to store, query, and process data locally, in the cloud, or at the edge in a resilient and responsive manner. The database uses a dynamic schema that supports both SQL and NoSQL operations as well as includes a built-in API to easily read and manage your data. It also includes a feature they call Custom Functions which gives us the flexibility of adding our own routes and code to the Fastify API under the hood. Additionally, HarperDB can serve our own static web UI, for example, a React or Angular frontend that uses Custom Functions to read and write data to HarperDB.

Signing up for HarperDB

The HarperDB Sign Up screen is shown, with the form filled out containing my name, instance URL and email.
Initial sign-up information required to open an account

A free tier is available for HarperDB which I’ve found is more than enough for most small projects and can be started by visiting the Sign-Up page. After setting a password, you’ll be redirected to the Instances page within HarperDB Studio, which is HarperDB’s management console. Inside HarperDB Studio, we can manage instances, manage our organization (users, billing, etc), view our data on each instance, among much more.

As we just started our account, we don’t have anything listed here, but we can create a new Instance by clicking the ‘Create New HarperDB Cloud Instance’ button. A popup will appear and ask you to choose between AWS/Verizon Wavelength or a User-installed Instance. I’ll choose to use an AWS/Verizon Wavelength instance as this way the Instance will be fully managed by HarperDB and the best part: I don’t have to set anything up!

HarperDB’s Instance type selection screen is shown. The User is given an option between an AWS / Verizon Wavelength Instance or a User-Installed Instance.
Instance Type selection — we’ll use an AWS Instance for this tutorial.

On the next window, choose ‘HarperDB Cloud on AWS’ and proceed to the next window to enter the Instance information. For the Instance name, enter ‘signs’ — this name will combine with your HarperDB Username to form your Instance’s URL as you can see below the text box. For the Instance Credentials, this account is used to perform administration tasks on the Instance such as adding new roles and Custom Functions. Most often, we’ll use this account when using HarperDB Studio. After we create the Instance, we’ll also set up a User who lacks ‘Super User’ permissions for the devices to use. Once you have entered your credentials, proceed to the next window to select the Instance details.

Instance specs window for HarperDB AWS Cloud Instances are shown. The free options are selected
Instance specifications selection since we are using a Cloud Instance

As long as you only select the free options, we can proceed to the next window and confirm our Instance details, otherwise, it’ll collect payment information. After clicking ‘Add Instance’, HarperDB will automatically provision our Instance in the HarperDB Cloud for us and let us know once it is ready by sending us an email.

Confirmation of Instance details window is shown, with the free options shown.
Final confirmation of Instance details

I’ve been using the free instance option for two months now and I’ve yet to experience any issues, such as downtime or failed queries. The response time is also incredibly respectable when compared to other free database solutions from my experience. I also really like that I can develop Custom Functions on a locally installed HarperDB Instance and then instantly deploy that code to a Cloud Instance, normally it would take a bit of scripting to handle that (or worst case — deploying it manually) so the time savings is much appreciated.

Configuring a HarperDB Cloud Instance

Once our HarperDB Cloud Instance has launched we can set up the initial schema and tables that are needed. First, create a new Schema and name it ‘signs’. Once we have added the Schema, HarperDB will let us create the tables housed within the Schema. As mentioned previously, HarperDB uses a dynamic schema so we will only provide the table name and hash attribute. The hash attribute is the column used to uniquely identify records. In addition to the hash attribute field, two attributes referred to as ‘Audit Attributes’ are added to each table: Created Time and Updated Time. This allows us to easily know when records were created or modified without having to add additional logic to our application. More information on HarperDB’s dynamic schema can be found in the documentation if you are interested.

The ‘Add schemas’ section in HarperDB is shown, with a schema named ‘signs’ being added.
Adding a new schema is super simple

Below where we created the initial schema, we can also add our tables here. We only have to do this once, as in the future we can copy this from Instance to Instance as needed.

HarperDB’s create table section is shown with one pre-existing table named ‘devices’ already created. A second table, named ‘logs’ with the hash attribute of ‘id’ is shown being added
Adding new tables to the schema is also really simple

The ‘devices’ table will hold our device information and configuration so that we do not need to adjust information on the device itself, we’ll be able to handle it all from the Management Console. The ‘logs’ table will hold any exceptions encountered by the Device as well as Heartbeats showing the Device as online.

A table is shown with two columns ‘Table Name’ and ‘Hash Attribute’. Row one has ‘devices’ for the ‘Table Name’ and ‘id’ for the ‘Hash Attribute’. Row two has ‘logs’ for the ‘Table Name’ and ‘id’ for the ‘Hash Attribute’.
Tables to create and their hash attributes (unique identifiers)

Creating a Device Role and User

For our Devices to access the Custom Functions, we’ll set up an extra user and role for the Devices to use which contain no permissions for reading, writing, and deletion. This is because we are only going to use this User to ensure we have valid credentials, much like an API key. To do this, from within HarperDB Studio, click the ‘Roles’ tab, and under ‘Standard Roles’ click the ‘+’ button. I’ll name mine ‘read_only’, but you can name it anything you’d like. If we needed to add permissions for certain tables we can add them in the right panel’s JSON object, however, we won’t need to add any for this use case. Once added, click the ‘Users’ tab and fill out a username, password and select the role previously created and keep it somewhere safe for later.

Cloning the Project

With the schema, tables, role, and the user created, we can move on to the fun part: the code! I’ve prepared a repository that contains all of the code we’ll need for this project. You can download the zip file or clone it using Git: https://levelup.gitconnected.com/media/6f9ede82200e07baf723e362b8ca338e

Within the repository will be a folder for the Custom Functions package, Frontend, User Interface, and scripts used in this guide. All of the code used within this project is NodeJS so you’ll need a Node environment installed locally. I suggest using NVM (Node Version Manager) to manage your node installation as it makes it much easier to manage. Instructions for installing NVM can be found in the documentation. To manage the packages for all of the projects I use Yarn, and the installation instructions can be found here. Alternatively, you could use NPM (bundled with Node) but Yarn is highly recommended and also guarantees the dependencies will work correctly.

Creating the Frontend Application

To make the administration of the signs easy, I wrote an Angular application that uses the Custom Functions setup previously to quickly and easily display and edit Devices. As the Custom Function endpoints are protected by authentication, I added a login screen and route guards to the application as well.

The project’s frontend is shown, with one device online and zero recent alerts. One alert is shown in the table.
Frontend with a sample error alert populated

On the dashboard of the application, the User is presented with some quick stats about their devices and any errors if present. Clicking on an error will allow the User to dismiss the error and go to the device of interest. Clicking on any device on the Devices list allows the sign assigned to the device to be updated or information about the sign to be changed, such as the description or name.

Code Explanation

In the project, the code is split into multiple modules: Login, Shared, and Devices. The login module contains only the login form component, while the Shared module contains the header, loading spinner, and modal components. Within the Devices module, the devices list and device detail components are housed. All of the modules contain a few services to handle interacting with the HarperDB Cloud Instance and storing/accessing the application’s state.

Building the Frontend

Before building the frontend in one of the next steps, the first thing you’ll need to do is update the environment files with your instance’s URL. This can be done by editing the files within the src/environments folder, for example: https://levelup.gitconnected.com/media/8be52869151eb95b03c89054fb946b27

If you are unsure of what your Instance’s Custom Functions URL is, it is displayed in the bottom left corner when viewing the Custom Functions tab, which we’ll explore in the next step. We don’t need to build the Frontend right now, as the deploy script will build it later automatically so we can move on to the Custom Functions.

Custom Functions

We’ll first need to enable Custom Functions on our Instance as they are not enabled by default. To do this, in HarperDB Studio, click the ‘Functions’ option in the top navigation and click the ‘Enable Custom Functions’ button. Once enabled, we’ll be able to create a new project, which you can think of like a namespace. It allows you to segment your code and help keep it organized and keep projects separated from one another. I’ll name my project ‘api’ to keep it simple, however, you can enter whatever project name you would like.

HarperDB allows us to deploy a Custom Function package to Instances, which we can take advantage of to load the controllers, routes, helpers, and frontend required for the project in one easy request. Normally, we would transfer the Custom Function from a locally installed Instance, however, we’ll use the HarperDB API instead to load it ourselves using a script I’ve prepared, which you can find in ‘scripts/deploy-custom-functions.js’.

The first thing we’ll need to do is update a few of the configuration constants. For example, I configured mine as so:https://levelup.gitconnected.com/media/80b56f3b480dc54d4b1d2171fc34e58b

Afterward, the script can be executed to perform the packing and uploading: https://levelup.gitconnected.com/media/b2623b785d04a9ceba934b20e2fe8fa3

With this step finished, we can navigate back to HarperDB Studio and see the new Custom Functions project that has been uploaded.


Each section of the project has its own routes file to help keep everything neat and separated as there are a handful of routes required for this project. Each helper is also separated into its file so if you wanted to copy one of them to your project it’s much easier in case there are dependencies required.

To protect the API routes, I wrote a helper (validateBasicAuth) that takes the authorization information provided by the User and checks it against the underlying HarperDB Instance. This way, to use any of the routes the User will have to pass a valid role and the endpoints are not open to the public. To ensure body parameters are present when editing the Devices, I also added another helper (checkBody) that ensures all required parameters are passed in the request body. Since some endpoints require multiple validators, I also added a method to chain validators together for one request (chainValidators), which was super helpful.

Creating the Device Application

To display a sign on the device’s display, we’ll need to create an application to handle checking for updated tasks and displaying the web content itself. I found the easiest method was using Puppeteer (Chromium) with options enabled to make the user interface more seamless, such as the Kiosk mode for example. It’s possible to use Webview instead, however, when compared to the rich feature set Puppeteer includes it was an easy choice as it will make our lives much easier when coding.


Upon launching the application, we’ll grab the latest task and then set up a timer to refresh that information every 10 seconds from one of the Custom Functions. To grab the device’s latest assigned task, we can use the ‘Get Device’ endpoint with our device’s unique ID. The loadTask method within the DigitalSignage class fetches this information and compares it to our active task. If a new task is detected, the application will clean up the previous tab(s) and start launching the new task assigned.

Browser configuration

To have the device behave more like a sign and less like a browser, there are some optional Chromium settings we can enable. We’ll also set up an event listener when launching so that if the browser were to close or encounter an error for any reason it can be relaunched as needed.https://levelup.gitconnected.com/media/962e4d7a13c083702d0a65cecfa05f15

Sign types

Within the application, all of the logic for how the signs are displayed is handled within each task’s class. For example, to show a series of web pages in a loop, we’d use the TaskWebSeries class. It’s straightforward to add additional sign types or additional features since each type is contained within its class.

Basic Web (TaskWebBasic)

The basic task is the simplest implementation, as it just displays a single web resource with an optional timer to refresh the page if needed. This task would be the most common, as it can serve many purposes.

Web Series (TaskWebSeries)

The series task is much like the basic task, but it can automatically switch between any number of pages. Each page can be configured to stay on the screen for a certain amount of time or configured to change all at the same rate.

YouTube Video (TaskYouTube)

The YouTube task automatically plays YouTube videos on full-screen mode easily. Upon launch, it’ll make sure the player starts playing and maximizes the video. Currently, it won’t skip ads but it would be a neat feature to add in the future.

Google Slides (TaskGoogleSlides)

This task allows you to display a Google Slides presentation automatically and cycle through the available slides at a predefined rate. The task can be configured to run through the slides continually in order or have the presentation reset to the beginning upon completion.

Error handling

If the device application encounters an error, it will retry most actions if applicable. If all retry attempts fail it will save the log message to the database using a Custom Function and allow you to view it on the homepage. This way, you do not have to set up a log collector or manually go to the device to check for errors.

Extra Setup

We’ll also install unclutter which will handle hiding the mouse. We’ll utilize systemd to manage the application and unclutter and ensure they keep running. As unclutter is not loaded by default, we’ll need to install it via the package manager after the device is set up. We’ll cover the steps for this during the ‘Final Setup and Configuration’ section.

Dry run and testing

We can now test the user interface by setting up a new device and assigning a task to it. Using the Management Console, navigate to ‘Devices’ and click ‘Add New Device’. From here, we can supply a name and description to remember this device by and select the sign task we’d like to assign to this device. For this example, I’ll use the ‘Web (Basic)’ sign type and give it a URL to load.

The Frontend’s ‘Edit Device’ screen is shown with the fields filled out to name the device as a test device and then ‘https://www.example.com’ set as the ‘Web (Basic)’ URL.
‘Edit Device’ screen used when adding/editing a Device

After filling out some sample details like above, click ‘Save Device’. Once you have saved your Device, you’ll now have some additional options available such as ‘Download Config’ and ‘Delete’. Go ahead and click ‘Download Config’, as we’ll need this shortly. A pop-up will appear and ask you for the device-specific credentials we set up earlier in the ‘Creating a Device Role and User’ section. The configuration stores this information so that the Device can authenticate against the API to fetch the latest tasks and upload any logs if needed.

If you’d like to test the User Interface before deploying it to a device, you’ll first need to copy the downloaded configuration to your home directory so the application knows where to find it (ex. /home/user/.sign_config.json). Afterward, navigate to the ‘user-interface’ folder within the article’s repository and run the following command: https://levelup.gitconnected.com/media/7620b71287a69d747b7ebc1bb176b896

After launching the application, the browser window should appear on your main monitor and navigate to the URL set up above. Later during the deployment of the application, we’ll add services to systemd to handle autostarting and restarting the application as needed.

Example.com is shown, as this was the requested URL set up.
Example.com loaded as per our configuration

Deploying to a Raspberry Pi

To deploy the application to a Raspberry Pi, we must first load an operating system onto the device. For this guide, I’ll be using DietPi as it is a lightweight Debian-based operating system perfect for tasks like this. From the Download page, select ‘Raspberry Pi’, or whichever board you are using, and download the appropriate image for your device. Once downloaded, using a program like Balena Etcher, flash an SD card with the image you downloaded and insert it into the Pi’s SD card slot.

After booting the Pi, the operating system will ask you to log in and accept the license. The default credentials are ‘root’ for the username and ‘dietpi’ for the password, which can be changed after you log in. Once accepted, it will run some initialization and updates as needed. It’ll also ask you to enable/disable the serial console, which you can disable if you’d like as it won’t be required for this project. Once completed, you’ll be presented with the DietPi-Software screen where you can easily install certain software, which we’ll go ahead and do.

Installing the Requirements

Using the arrow keys, select ‘Search Software’ and follow these steps:

  1. Type lxde and press the Enter key.
  2. With the ‘LXDE’ option highlighted on the next screen, press the ‘Spacebar’ button to select it.
  3. Press the ‘Tab’ key and press the ‘Enter’ key when ‘Ok’ is highlighted.
  4. Select ‘SSH Server’ and use the arrow keys to select ‘OpenSSH Server’ and accept the confirmation.
  5. Using the arrow keys, select ‘Install’ at the bottom of the screen, and then select ‘Ok’.
  6. When prompted for the web browser, select ‘Chromium’.
  7. When prompted to increase the GPU memory, select ‘Ok’.
  8. Once the software has been installed and you are back at the command line, enter the command dietpi-config .
  9. Select ‘AutoStart Options’ from the list and under ‘Desktops’, select ‘Automatic Login’ and then choose the User ‘dietpi’.
  10. Press the ‘Tab’ key and select ‘Exit’ and press ‘Enter’ to exit.
  11. Type startx and press ‘Enter’ or reboot the device to start the desktop environment.

Installing Extras

To install Node on the device, I suggest using NVM. It can be easily installed on the device by following these instructions in a command terminal or using the ‘install_nvm.sh’ script within the scripts folder inside of the User Interface folder: https://levelup.gitconnected.com/media/d44c70dee0af201ecdd5057ffc522f07

Extra Steps

  1. At the desktop, right-click anywhere on the desktop and select ‘Desktop Preferences’.
  2. Under the ‘Wallpaper Mode’ option, select ‘FIll with background color only’, and click ‘Close’. Optionally, you can also remove all of the desktop icons and set the bottom task bar to minimize when not in use via the taskbar settings for a cleaner look.
  3. If you are not using DietPi — you’ll also want to disable screen blanking and any screensaver options.
  4. If you wanted to use wireless versus wired:
    a. In a terminal, enter the command sudo dietpi-config.
    b. Navigate down and select ‘Network Options: Adapters’. Select ‘Onboard WiFi’ and select ‘Ok’ to enable it.
    c. Select ‘WiFi’ and select ‘Ok’ to enable it.
    d. Afterward, select ‘Scan’, select the first slot, and select ‘Manual’.
    e. Enter your wireless credentials when prompted and select ‘Done’ and then ‘Apply’ and accept the confirmation.
    f. Exit out of the configuration menu and restart the Pi when prompted.

Final Setup and Configuration

To copy the files to the device, we’ll first need to build the User Interface on your local machine and then transfer the compiled code to the Device. If you are unsure of what the IP Address is for your Device, you can open a command terminal and DietPi will display it at the top. https://levelup.gitconnected.com/media/2676079e28e4c272d0b4a7cc1e8c4942

With all of the files copied, we can now do some of the final setup required on the Device to get it ready. https://levelup.gitconnected.com/media/f9d93f6844814096b5d3710376637644

We should now see the sign setup earlier displayed on the Device’s screen. Afterward, the sign can be modified and adjusted to a different URL or type to see how it handles updating. I’ve included a sample restaurant menu as well, which you can use by setting the URL as: file:///opt/signs-user-interface/templates/menu_example.html .

Menu example output as shown on the screen of the Device


Overall, HarperDB makes this project more streamlined since it handles multiple responsibilities, which saves us a lot of trouble. With the User Interface, some other improvements could be made further, such as remote control of the device to restart it or pull the system logs if needed. Another aspect that would be good to monitor would be the device’s statistics such as free memory, CPU temperature, etc.

I’m personally looking forward to interacting with HarperDB some more and building additional projects as it was much easier when deploying projects. If you have any questions about the code or HarperDB, feel free to leave a comment as I’d be happy to answer!

Links / Resources