<- All posts

How to Build a Compliance Management System in 6 Step

Ronan McQuillan
19 min read · Feb 4, 2025

Businesses today face a hugely complex, varied regulatory environment. Between national, international, local, and internal policies, managing compliance can be a massive undertaking.

Adopting an appropriate compliance management system is critical. Specifically, we need a singular, centralized solution for handling key data related to our compliance efforts.

The trouble is every business faces unique compliance challenges. We’re all constrained by different internal policies, with distinct processes that must adhere to them.

Today, we’re exploring one solution, by checking out how Budibase can be used to build a custom compliance management tool.

By the end, you’ll have a fully working tool that can be adapted to meet your exacting requirements.

Let’s start with the basics.

What is a compliance management system?

A compliance management system is an internal tool that’s used to store information relating to applicable polices, as well as managing any workflows required to maintain adherence with these.

For example, handling issues detection, auditing, policy discovery, or other relevant processes.

However, the scope of this can vary quite widely. So, some solutions might provide organization-wide compliance management, while others are focused on a specific department or business vertical.

Many tools are focused on even more granular use cases - such as managing security or privacy regulations within IT departments.

Often, these kinds of platforms can provide more advanced capabilities that support their core use case. So, in a security-focused context, we might see automated endpoint monitoring or other threat detection tools.

Take a look at our round-up of IT risk management tools to learn more.

In any case, the core pain point that compliance tools solve is providing a robust, centralized view of the policies that we’re bound to, along with streamlining the workload required to ensure we adhere to these.

So…

What are we building?

We’re building a simple, web-based compliance system, based around a two-role workflow.

This will enable:

  • Compliance Managers to manage policy data, request audits, and implement follow-on actions.
  • Auditors to view policies, carry out assigned audits, and recommend follow-on actions.

We’re building this by connecting to an external Postgres database and using Budibase to build interfaces, create user roles, and handle access control within our workflow.

We’re taking a lightweight approach to our data model in order to give you the flexibility to modify any of our data objects to suit the schema you need for your own workflows.

We’ll provide all of the queries you need to create our database and build along with this tutorial a little bit later.

Budibase also gives you the power to choose how and where you deploy your internal tools. We offer a cloud platform, as well as optional self-hosting on your own infrastructure.

How to build a compliance management system in 6 steps

Let’s get building. If you haven’t already, sign up for a Budibase account to create as many applications as you want for free.

Join 200,000 teams building workflow apps with Budibase

1. Setting up our data model

The first thing we need to do is create a new Budibase application. We have a few options for doing this, including importing an existing app dump or using one of our pre-built templates. But today, we’re starting from scratch.

When we choose this option, we’re prompted to give our app a name. This is also used to generate a URL extension.

We’ll call ours Compliance Management System.

Compliance Management System

Connecting to our database

Once we’ve created our app, we need to set up a database.

We’re presented with a number of options, including connecting to an RDBMS, NoSQL tool, or spreadsheet. Or, we could start from scratch using BudibaseDB.

Data Sources

As we mentioned earlier, today, we’re using an existing Postgres table. When we choose that option, we’ll be asked to input our configuration credentials.

Config

We’ll then be asked which of our database tables we’d like to fetch, making them queryable within our app. Our database has three tables called action, audit, and policy. We’re fetching all three.

Fetch Tables

If you want to create these tables in your own Postgres database, you can use the following query.

 1-- Create the Policy table with auto-incrementing ID
 2
 3CREATE TABLE Policy (
 4
 5  id SERIAL PRIMARY KEY, -- Auto-incrementing integer ID
 6
 7  name TEXT NOT NULL,
 8
 9  description TEXT,
10
11  effective_date DATE NOT NULL
12
13);
14
15-- Insert sample data into Policy table
16
17INSERT INTO Policy (name, description, effective_date)
18
19VALUES 
20
21  ('Data Protection Policy', 'Ensures compliance with GDPR', '2023-01-01'),
22
23  ('Security Policy', 'Guidelines for access control and encryption', '2023-02-15'),
24
25  ('Incident Response Policy', 'Procedures for handling security incidents', '2023-03-10');
26
27-- Create the Audit table with auto-incrementing ID and foreign key to Policy
28
29CREATE TABLE Audit (
30
31  id SERIAL PRIMARY KEY, -- Auto-incrementing integer ID
32
33  policy_id INTEGER REFERENCES Policy(id) ON DELETE CASCADE, -- Foreign key to Policy.id (integer)
34
35  date DATE NOT NULL,
36
37  status TEXT CHECK (status IN ('Planned', 'In Progress', 'Completed')) NOT NULL,
38
39  findings TEXT
40
41);
42
43-- Insert sample data into Audit table
44
45INSERT INTO Audit (policy_id, date, status, findings)
46
47VALUES 
48
49  ((SELECT id FROM Policy WHERE name = 'Data Protection Policy'), '2024-01-10', 'Completed', 'Minor issues found in data retention.'),
50
51  ((SELECT id FROM Policy WHERE name = 'Security Policy'), '2024-02-05', 'In Progress', 'Encryption policies not fully implemented.'),
52
53  ((SELECT id FROM Policy WHERE name = 'Incident Response Policy'), '2024-03-01', 'Planned', NULL);
54
55-- Create the Action table with auto-incrementing ID and foreign key to Audit
56
57CREATE TABLE Action (
58
59  id SERIAL PRIMARY KEY, -- Auto-incrementing integer ID
60
61  audit_id INTEGER REFERENCES Audit(id) ON DELETE CASCADE, -- Foreign key to Audit.id (integer)
62
63  description TEXT NOT NULL,
64
65  due_date DATE NOT NULL,
66
67  status TEXT CHECK (status IN ('Pending', 'In Progress', 'Resolved')) NOT NULL
68
69);
70
71-- Insert sample data into Action table
72
73INSERT INTO Action (audit_id, description, due_date, status)
74
75VALUES 
76
77  ((SELECT id FROM Audit WHERE findings LIKE '%data retention%'), 'Revise data retention policy', '2024-02-15', 'In Progress'),
78
79  ((SELECT id FROM Audit WHERE findings LIKE '%Encryption%'), 'Implement full-disk encryption', '2024-03-10', 'Pending');

Here’s how these will look within Budibase’s Data section.

Table

We’re going to use the Data section’s grid-like interface to make a few tweaks to this so that when we start generating UIs, they’ll better reflect our needs.

Firstly, all of our textual attributes are stored with the same TEXT data type, as Postgres doesn’t distinguish different types of strings based on their length.

However, there are some columns where we’re going to want to give end-users a little more space in form fields. These are the description columns in the action and policy tables and the findings column in the audit table.

Under each of these, we’ll hit Edit Column and change the type to Long Form Text.

long form text

Make sure to repeat this process for all three columns.

Secondly, both our audit and action tables have status attributes with the TEXT type. So, we’ll follow the same process as before, this time setting their type to Single Select. This time though, we’ll need to hit Add option to populate our possible values.

For the audit status, our options will be:

  • Planned
  • In Progress
  • Completed

We’re also setting our default value to Planned so that this will automatically be populated when a Compliance Manager schedules an audit.

Enum

We’ll repeat this process on the action table, setting our options to:

  • Pending
  • In Progress
  • Resolved

Our default value this time will be Pending.

Pending

Setting up relationships between tables

Next, we need to define the relationships between our three tables. The queries we used to create these already provided primary and foreign keys to handle these.

Specifically:

  • The audit_id attribute in the action table corresponds to id in the audit table.
  • The policy_id attribute in the audit table corresponds to the id attribute in the policy table.

We just need to set these up in Budibase. We’ll start by hitting define relationship on the action table.

Relationships

Then, we’ll link many rows in the action table to one row in the audit table, using audit_id as our primary key and id as our foreign key.

Configure Relationship

We’ll then repeat this same process to link the audit table to the policy table.

Compliance Management System

We’ll now be able to call and interact with related rows when we’re using our table data.

Relationships with the users table

We also need to link each of our external tables to Budibase’s internal Users table. For this, we have two special data types called Single User and Multi User. We’re going to add several Single User columns across our three tables.

Firstly, we’re going to add a column called createdBy to each table. We’re also enabling the option do default to current user. This will populate a value that corresponds to the user that creates each new row.

Single User

Again, make sure you complete this process for all three tables.

On the audit table, we’re going to add a second Single User column and call it assignedTo. This time, we’re not using a default value, as we want Compliance Managers to assign Auditors manually.

Assing User

We’re also going to assign ourselves to a couple of rows here for testing purposes in the following section on RBAC.

Assign user

Configuring extra columns

Before we get to that, we’re going to add one additional attribute within our data model. On the audit table, add a column and call it title, this time selecting the Formula type. We can then hit the lightning bolt icon to create our formula.

img

This will open a modal screen where we can enter either a handlebars or JavaScript expression. We’re using the following handlebars to print the name of the policy the audit relates to, as well as its due date.

1{{ policy.0.name }} Audit {{ date }}

Formula

We’ll then save our formula and designate title as our Display Column for this table.

Display Column

Note that in the screenshot above, this option is disabled because we’ve already set it as our display column.

2. User roles and data access

Now that we have our database set up how we need it, we can start creating access rules for our compliance management system.

Creating custom user roles

First, we need to create our user roles. As we said earlier, these will be called Compliance Manager and Auditor. Eventually, we’ll give Compliance Managers full CRUD access for all of our tables, while Auditors will only be able to take more specific actions.

Within the Data section, hit Manage Role to open our visual RBAC editor.

RBAC Editor

We’ll start by hitting Add Role to create Compliance Manager.

Compliance Manager

We’ll then repeat this process to create Auditor.

Auditor

We can reposition these to establish inheritance between roles. So, we’ll drag Auditor to the left of Compliance Manager and then draw a line between them. This means that Compliance Managers will inherent all permissions that we give to Auditors, as well as their own ones.

Inheritence

Configuring access to database tables

With our roles created, we can start granting permissions to each of them. As we said earlier, we want to give our Compliance Managers full read and write access across all data.

So, on each of our three tables, we’ll hit Access and choose Compliance Manager.

Assign Access

Repeat this process for all three tables.

For our Auditors, we need to set more granular access, as they’ll only be able to read and write specific columns and rows across our tables. We’re going to achieve this with custom Views.

First of all, head to the policy table and hit Create a View.

Create View

We’ll choose the Table option and call our view Auditor Policy.

Table

Our Auditors will have full read access for all rows. Under Columns, we’ll set description, audit, and createdBy to read-only, as well as hiding id. We have to leave name and effective_date as writable, as they’re required fields, but we’ll disable these in our UI later.

Columns

Then, we’ll set the access to Auditor.

Auditor

Next, we’ll create an Auditor Audit view and again set its access to Auditor. This time, we only want users to be able to access the rows that are assigned to them. So, we’ll hit Filter.

Filter

We’ll add a rule for the assignedTo attribute and then use the lightening bolt to open the bindings drawer.

filter

And under Current User we’ll select the _id binding.

Current User

Now, we can only see the subset of rows that we assigned ourselves to earlier.

Filtered Rows

And we’ll configure our columns just like we did earlier.

This time, we’ll make policy_id and policy read-only and hide id, action, createdBy, and assignedTo. This leaves only findings, date, title, and status writable.

Compliance Management SYstem

Lastly, we’ll create Auditor Action, again remembering to set our access to Auditor.

This time, we’ll add a filter based on Audit.assignedTo. This is the assignedTo attribute from the related row in the audit table. So, they’ll only be able to access and create actions against the audits that are relevant to them.

We’re binding this to {{ Current User.globalId }}.

Current User

We’ll hide audit_id and make created_by and status read only, since these have default values already that we don’t want Auditors to overwrite.

Columns

3. Build a CRUD UI for policies

Now that we’ve configured our data model and our access roles, the rest is easy. In Budibase, we can autogenerate user interfaces.

These will inherit the permissions that we’ve applied to the underlying tables and views we generate them from.

Start by heading back the policy table in the Data section. Here, we’ll hit Generate and select Table under App Screens.

Generate Screen

When we do this, we’re given a few options for how we’d like to present the edit and create row forms on our new screen. We’re choosing the Modal option.

Modal

Here’s how this will look in the Design section. Note that our screen already has its access role set to Compliance Manager, the same as the underlying table we generated it from.

Crud UI

We’re going to make a few minor adjustments to this. First of all, we’ll select the Table component and use the sliders on the right to hide the id and createdBy attributes.

Columns

We also want to remove unnecessary fields from our two Form Block components. So, we’ll select create row form block, this time hiding the id and audit attributes.

Form

We can also make adjustments to our forms to make better use of the vertical space. We’ll open the settings for our name and effective_date fields, and arrange them into two columns under Layout.

Layout

Under Styles, we’ll set our Button Position to Top.

Button Position

We’ll also update the form’s Title to something a little more descriptive.

Title

We’ll then repeat this exact same process for our other Form Block.

Form

In terms of core CRUD functionality, our policies screen is ready to go.

However, we also want to give Compliance Managers the ability to schedule an audit of any policy, including setting a date and assigning an Auditor.

We’ll start by adding a blank Modal component, which we’ve named Audit Modal.

Modal

Inside this, we’ll add a Form Block, setting its Data to audit.

Form

We only need Compliance Managers to add a small subset of fields when they create an audit row. So, we’ll deselect everything except policy, assignedTo, and date.

Form

We’ll arrange assignedTo and date into columns, then change our button position and update our title once more.

Button

We don’t want users to have to populate the policy field manually. Rather, we’re going to set this up so that it’s already filled in to reflect the row they clicked to open our modal.

To achieve this, we’ll head back to our Table component and add a button. We’ve set the Text for this to Schedule Audit.

Actions

We then need to configure what this button will do. Under its settings we’ll open the actions drawer, first adding a step called Update State.

Update State

This will store a key/value pair in the background of our app.

We’re setting our Key to policyToAudit, to make it obvious what this does. We’ll bind our Value to {{ policy - Table.policy._id }}. This is the unique identifier of the row the user has clicked.

Bindings

Then, we’ll add a second step called Open Modal and point this at our Audit Modal.

Open Modal

Back to our audit form, we’ll select the policy column and give it a default value of {{ State.policyToAudit }}, and then select the Disabled option.

Compliance Management System

Now, a policy value is auto-populated and displayed, but users can’t overwrite this.

And that’s our policies UI ready to go.

4. Adding a UI for auditors

Once an audit has been scheduled, the next step in our workflow is for this to be picked up by the assigned auditor. They can then alter its status, add findings, or create related action rows.

We’ll start by generating a Table screen with modal forms based on our Auditor Audit view.

CRUD SCREEN

We’ll be making more extensive changes to this screen compared to the last one. For a start, auditors don’t need to add rows, so we’ll delete this Modal and the corresponding button.

Delete

Unlike our previous screen, we want to display certain values from the related policy role. To do this, we’ll head back to the data section and select our Auditor Audits view.

Under the related policy column, we’ll add effective_date, name, and description as readable attributes.

View Join

Back in the Design section, we’ll add policy.name and policy.description to our table. We’ll deselect policy_id, policy, findings and policy.effective_date.

Columns

Because of the underlying data, this table is already filtered to only include rows that are assigned to the current user.

However, when a Compliance Manager navigates to this screen, we’d like to display all rows.

We can achieve this using Budibase’s conditionality feature. Select the Table component and hit Conditions, where we can open the conditionality drawer.

Create Conditions

We can use this editor to display, hide, or update any native setting of our component based on any data it’s exposed to.

We’re going to create an Update Setting rule, that will set our Data setting to the unfiltered audit table.

Data

This will be applied if {{ Current User.roleId }} equals {{ Role.Compliance Manager }}.

Condition

Next, we’ll start updating our edit row form to better suit our workflows. Specifically, we want users to be able to do the following in addition to the existing actions on the relevant audit row.

  1. View the policy that the requested audit applies to.
  2. Register related actions.
  3. View existing related actions.

On the existing form, we’ll start by deslecting the policy and policy_id attributes. We’ll also set date to disabled, and set it and status to be arranged in columns. We’ve also removed the delete button and bound the Title to {{ Edit row form block.Auditor Audit.title }}.

Disable

Now, our users can only update the status and findings fields to complete an audit.

From this modal, we also want to give users the option to read the relevant policy. We’ll start by placing a Side Panel component below our existing Modal.

Side Panel

Inside this, we’ll place a Form Block with it’s Data set to Auditor Policy and it’s Type set to View. We’ll deselect all fields except effective_date and description, and bing our Title to {{ Policy Form Block.policy.name }}.

Form

We need to configure this so that it loads the appropriate policy row for the audit row that a user is viewing, using the Row ID setting.

We’ll bind this to the policy._id output of our audit for block, using {{ Edit row form block.Auditor Audit.policy.0._id }}.

Bindings

We’ll also add a Button to our form and call it Close. This will trigger a Close Side Panel action.

Close Side Panel

Back on our audit form, we’ll add a corresponding button and set its Text to Policy. This triggers an Open Side Panel action, pointed at our new Policy Side Panel.

Open SIde Panel

Next, we want to start building a UI for handling actions. We’ll start by adding a container beneath our audit form. We’re calling this Center Container, and setting it’s Horizontal Alignment to Center.

Alignment

Within this, we’ll nest a second container called Width Container and set its Width to 600 to match our form.

Width

Inside this, we’ll add a Table component, with its Data set to Auditor Actions. We’re also giving this a fixed Height of 200.

Table

We’re deselecting all columns except due_data and status.

Columns

Next, we need to create a form UI for auditors to submit actions against the current audit row.

We’ll start the same way as we did before, by creating a Side Panel component with a Form Block nested inside it. This time the form’s Data is Auditor Actions and its Type is Create.

Form Block

We’ll hide createdBy and status since these have default values, as well as setting audit to disabled and repositioning our button.

form

Then, we’ll give audit a default value of {{ Edit row form block.Auditor Audit._id }}.

Compliance Management System

Back in our Modal, we’ll add another Container about our Table. We’ll call this Headline Container and place a Headline component within it to read Actions.

Headline

Next to our Headline, we’ll add a button with the display text Add Action.

Button

Lastly, we’ll set this to open our new side panel.

Open Side Panel

5. Audit outcomes and actions

Our last screen will be used by Compliance Managers to manage actions that have been recommended by auditors.

We’ll start by generating one last Table screen with Modal forms based on our actions table. This time, we’re leaving all columns enabled except id and audit_id.

Columns

Once again, we’re going to remove our Create button and corresponding modal/form.

Delete

This time, we’ll place an Options Picker component where our button used to be, with its Field set toAudit.

The idea here is to enable our Compliance Managers to filter recommended actions based on the audit row they’re associated with.

This means we need to populate the options that we offer to end-users dynamically in order to take account of new audits as they’re created, without needing a Budibase admin to add these.

To do this, we’ll start by adding a component called a Data Provider at the bottom of our screen. This accepts a data source, and exposes all of the other components on the screen to it via bindings.

This will be invisible to end-users.

We’ve pointed Data Provider at the audit table.

Data Provider

Back to our Options Picker, we’ll set the Options Source to the output of our new Data Provider. For the Label Column, we’re using the title attribute and for the Value Column, we’re using id.

In other words, end-users will select an audit by their title, but the Options Picker will output the unique identifier of the corresponding row.

Options

Then, we’ll add a condition to our table that applies a filter if our Options Picker is not blank.

Options Picker

We’ll set our filter rule to return rows where audit_id equals {{ Audit Options Picker.Value }}.

Filter

Lastly, we’ll head back to our modal form, update our display text, reposition the button, and arrange our fields into columns.

We’ve also disabled all fields except status.

Compliance Management System

6. Design tweaks and publishing

From a functional perspective, our compliance management system is ready to go. However, before we push it live, we’re going to make a few UX tweaks.

First of all, we’ll head to Screen and Theme, where we’ll choose Midnight.

Theme

In the same panel, we can update our button colors to match the Budibase brand better.

Button Color

Under Navigation, we can make any adjustments we’d like to the display text for our menu items.

Nav

Similarly, we can update the display text of all of our table columns and form fields to be more human-readable rather than simply using the names of our attributes as they’re stored in our underlying database.

Display Text

Once we’re happy, we can hit Publish to push our new application live.

Publish

Here’s a reminder of what the finished product should look like.

Turn data into action with Budibase

Budibase is the fast, easy way to build secure applications for handling IT workflows. With extensive data support, autogenerated UIs, optional self-hosting, free SSO, and more, there’s never been an easier way to ship internal tools at pace.

Take a look at our features overview to learn more.