How to Build a CRM with 黑料正能量

Building a CRM is one of the most common custom software projects you鈥檙e likely to encounter.
See, every customer-facing business needs a repeatable way to manage relationships with current, potential, and previous clients.
The trouble is that the market here is so thoroughly cornered by a handful of big players - like SalesForce, Dynamics365, HubSpot, and others.
This creates two potential problems. First, with such market dominance, these vendors can often present comparatively high recurring licenses.
Second, with an off-the-shelf solution, there鈥檚 no guarantee that the capabilities on offer will be a good fit for your specific requirements. In other words, you risk paying for features you don鈥檛 need or being forced to make do without features that you do need.
The relative benefits and costs to build a CRM system that matches your distinct business processes can make this a much more attractive option.
Today, we鈥檙e looking at an alternative approach, as we explore how to build a lightweight, custom CRM system from scratch using 黑料正能量.
Let鈥檚 jump right in.
How to build a CRM in 6 steps
We鈥檒l kick off by outlining what exactly we want to build. It鈥檚 worth pausing for a second and reflecting on what a CRM is. In practice, there is a huge range of different types of CRM systems - aimed at distinct use cases and market segments.
But when we zoom out a little bit, the core purpose is quite simple.
CRMs are tools that help us manage and leverage customer data. Ultimately, the goal is to give our marketing and sales team members a clear, accessible picture of our customer鈥檚 journey as they interact with different channels and assets.
Therefore, the more closely your CRM鈥檚 design matches your real-world sales processes, the better - so we may turn to the development of a custom CRM solution.
The tool we鈥檙e going to teach you to build is based around a purchasing journey with three distinct phases:
- New leads - potential customers who have reached out to our sales team.
- Opportunities - leads that have been contacted and verified by our sales team.
- Customers - Sales that have been completed.
Each of these will have its own dedicated UI - with full CRUD functionality and customizable analytics to display customer reports.
On top of this, we鈥檙e going to build an embeddable lead capture form that automatically categorizes leads and notifies our team. We鈥檒l also add a customizable analytics dashboard for breaking down our data by lead status and channel.
By the end of this guide, you can have a fully functioning custom CRM that you can connect to just about any customer data source you鈥檇 ever want to.
Without further ado, let鈥檚 build a custom CRM.
1. Create a 黑料正能量 app and connect your data
The first thing we鈥檙e going to need to do is create a new 黑料正能量 application and give it a name.
We鈥檒l call ours CRM:
This will also populate the URL slug that can be used to access our application - although we could edit that if we wanted to.
Next, we鈥檒l be prompted to choose a data source. 黑料正能量 offers a huge range of options here, to suit wherever your customer data is currently stored.
We鈥檙e going to use Postgres for our demo app:
When we click through here, we鈥檒l be prompted to add our database credentials:
Then, 黑料正能量 will ask us which specific tables within our Postgres schema we want to fetch so that we can manipulate them within our CRM app. Our existing customer database actually has quite a few tables that we don鈥檛 need - but we could leverage them if we needed more advanced CRM features.
We want to create a fully operational CRM around online sales - but we could just as easily add in some customer satisfaction metrics into our data model.
We鈥檙e only going to select and fetch two:
- contacts - which stores information about the users who have completed our contact sales form, along with their subsequent interactions with our team.
- companies - which stores information about companies that might be related to these users.
Having fetched these, we already have full CRUD functionality in 黑料正能量鈥檚 backend data UI, using our editable tables:
But, before we move on to building front-end interfaces for our users, we want to make a couple of adjustments to the data model that鈥檚 been imported.
First, the contacts table has a status attribute.
In Postgres, this is treated as a string, but when users interact with our CRM data, we only want them to have a fixed set of options - since the status of a contact is ultimately what denotes its current stage in the purchase process -in real-time.
To achieve this, we鈥檒l hit the three dots icon beside the status attribute and choose edit column. Then, we can change its type to options and populate the choices we want to offer to users.
We can even color-code each one.
Now our table looks like this:
The second thing we need to do is define the relationship between our companies and contacts tables. Each one already has a unique ID column, which we can use as our primary and foreign keys.
We鈥檙e going to create a relationship where one companies row can be related to many contacts rows.
We鈥檒l start by heading to the companies data tab and hitting define relationship. Then, we鈥檒l use the following configuration to create a one to many relationship, linking the two tables on the companyId attribute:
Finally, we can choose which attribute in each table we want to use to represent rows in the other table. This is what鈥檚 called the display column. For instance, Under each companies row, we鈥檇 like any linked contacts rows to show up as the customer鈥檚 email address.
Head back to the contacts table - and beside the email attribute, hit the three dots and select use as display column:
Now our two basic data tables are ready and we can start building interfaces.
2. Build an opportunities screen
The first screen we鈥檙e going to build is where we鈥檒l handle our opportunities. Remember, these are the leads that are currently in progress.
Here鈥檚 what the completed UI will look like in-situ:
As you can see, the screen has two main sections:
- Three cards that give summary information about our opportunities pipeline.
- A table that lets us view and edit details of individual opportunities.
We鈥檒l start by adding a blank screen in the design section and calling it Opportunities.
Building our data table
Next, we鈥檒l click add component to add a table block component and set its data to contacts:
We also want to add a filter, so that this table only shows the rows with a status attribute that equals In Progress:
Now, our table displays the right rows, but it鈥檚 a bit overwhelming. We want to cut the number of columns that are displayed so that we鈥檙e not overloading our users with information.
We can use the columns drawer to define which fields our table is going to display:
But, our table only gives us a way to read opportunities information in its current state. We also need to provide users with a way to edit rows.
To do this, scroll down and tick the show details side panel option:
Now, if a user clicks on a row, they can access a form to edit or delete information:
And that鈥檚 our opportunities table UI complete. Next, we want to create the statistics cards that we saw in our completed screen.
Custom queries
These cards will display three pieces of information:
- The total estimated value of all of our opportunities.
- The average estimated value.
- The number of opportunities we鈥檝e had contact from in the past seven days.
One query will handle the first two data points, and then we鈥檒l use a second query for the third.
To do this, we鈥檙e going to need to head back to the Data section, and create a couple of custom queries under our Postgres data source.
Then, we鈥檙e presented with a new screen where we can give our query a name, and enter our custom SQL command.
First, we鈥檒l create a query called AvgAndTotalValue. As the name suggests, this will give us the average and total estimated value of our opportunities.
Here鈥檚 the code we鈥檒l use in our query:
1SELECT
2
3 SUM(EstimatedValue) AS TotalEstimatedValue,
4
5鈥 AVG(EstimatedValue) AS AvgEstimatedValue
6
7FROM
8
9 Contacts
10
11WHERE
12
13Status = 鈥In 笔谤辞驳谤别蝉蝉鈥
We鈥檙e using the built-in SUM() and AVG() functions in Postgres - along with a WHERE clause so the query only returns row with the In Progress status
And here鈥檚 what the whole thing will look like in situ:
When we hit run query it will return a JSON object with two key/value pairs - totalestimatedvalue and avgestimatedvalue:
We鈥檒l save that and move on to our other query for finding the number of contacts we鈥檝e had contact with in the past seven days. This one will be called Last7DaysContact.
This time, we want to use the COUNT() function to return the number of rows where the LastContact attribute is within the past seven days, and the status attribute is set to In Progress.
Here鈥檚 the query we鈥檒l use:
1SELECT COUNT(*) AS RecentContactCount
2
3FROM Contacts
4
5WHERE LastContact >= CURRENT_DATE - INTERVAL '7 days'
6
7AND LastContact < CURRENT_DATE + INTERVAL '1 day'
8
9AND Status = 'In Progress';
And here鈥檚 what it looks like in 黑料正能量:
When we run that, we get a single key/value pair for recentcontactcount.
Hit save and head back to the design section.
Stat cards
We鈥檙e going to start by adding a container component above our existing opportunities table. We鈥檒l set its direction to horizontal:
Inside this we鈥檒l place a Cards Block component, with its data set to our AvgAndTotalValue query:
Then, we鈥檒l bind its Title field to the totalestimatedvalue output of our query:
We鈥檒l edit that slightly by adding a 拢 symbol to the front of the handlebars binding, as well as using the toFixed helper method to give us two decimal places.
The new title binding is:
1拢{{ toFixed New Cards block.AvgAndTotalValue.totalestimatedvalue 2}}
We鈥檒l set the card鈥檚 subtitle field to Total Estimated Value while we鈥檙e here and rename the Cards Block to TotalValueBlock in order to avoid confusion.
Here鈥檚 what we have now:
For the average estimated value, we鈥檒l simply duplicate this first card:
Now, we can swap out the appropriate data. So, set the name to AvgValueBlock and the subtitle to Average Estimated Value.
Then, we can use the following handlebars binding for the title:
1拢{{ toFixed New Cards block.AvgAndTotalValue.totalestimatedvalue 2}}
And here鈥檚 what we鈥檝e got now:
Finally, we鈥檒l add one more card block with our Last7DaysContact query as its data source. We鈥檒l call this one Last7DaysBlock and set its subtitle to Contacted in the last 7 days.
We鈥檒l use this binding for its title:
1{{ Last7DaysBlock.Last7DaysContact.recentcontactcount }}
Here鈥檚 our screen so far:
We鈥檒l tweak the design of our entire app in a little bit, but for now that鈥檚 the first screen completed.
3. Embeddable lead capture form
Next, we need a way to capture new lead鈥檚 data in our CRM. On the surface, this will be a relatively simple form, but we鈥檙e going to use a combination of 黑料正能量鈥檚 built-in RBAC and embeddable apps functionality to make something a bit more sophisticated.
We鈥檙e going to start by creating another blank screen, with the URL slug:
1/contact-form/:channel
When we use a semicolon in a screen鈥檚 URL, it gives us a dynamic variable. So, :channel is just a placeholder that we can populate with whatever actual channel we want to later, when we embed our app.
We鈥檒l then be able to access this a bindable value in our UIs.
Hit continue and then when prompted set the Access Level to Public:
This means anyone will be able to access this particular screen - even if they don鈥檛 have an account associated with your 黑料正能量 tenant.
When unauthenticated users see the embedded version of the app, this is the only screen they鈥檒l be able to access.
But, we鈥檒l get to embedding the app in few minutes.
Building our basic form
We鈥檒l start by center aligning everything in our new UI鈥檚 screen space:
Then, we鈥檒l add a Form component, which is going to wrap around our lead capture form fields.
We鈥檒l call this Contact Form and set its schema to contacts:
Inside this, we鈥檒l nest a Field Group component and hit Update form fields:
This will automatically create form fields for every attribute in our contacts table.
Obviously, we don鈥檛 want users to add all of these, so we鈥檒l delete everything except first name, last name, email, and phone:
For the other fields, we鈥檒l either populate these automatically when the row is saved or as the new lead works its way through our CRM鈥檚 data flow.
Next, we want to tidy up the design a bit. We鈥檒l start by capitalizing the Label and Placeholder for each form field component and setting their widths to 400px:
Then, let鈥檚 add a headline component above our field group and a submit button below:
Defining button actions
Next, we need to configure what our button does. It鈥檒l actually do a couple of things:
- Save the row - including populating certain values that 补谤别苍鈥檛 included in the form.
- Make the form disappear once a new lead has been submitted - to confirm that their details have been recorded.
We can use the side panel to open the on-click actions drawer:
We鈥檒l start by adding a save row action with the table set to contacts:
Then, we鈥檒l hit the Add Columns button to save a few other attributes in addition to what鈥檚 prodived in the form.
First, we鈥檒l set the status to New:
Then, we need to set the channel attribute to whatever is bound to the :channel element of our URL:
So we鈥檙e using {{ URL.channel }} as a bindable value. This means that when we save a row, the channel attribute will be set to whatever we provide in the UI when we embed to form. We鈥檒l return to this in a second.
At this point, we鈥檙e also going to head to the theme section of 黑料正能量 and set our app鈥檚 color scheme to Nord - to make it a bit more attractive for prospective customers:
We鈥檒l also disable the show nav option for this screen.
In order for a submission to make the whole form disappear, we need to add another action to our button. This time, we鈥檙e using the update state action. We鈥檒l add a new key called form-submitted and set this to yes when someone clicks the button:
Then, we鈥檒l select our Form component again and hit configure conditions. We鈥檒l create a conditonality rule so that the whole form is hidden is form-submitted equals yes:
Then, we鈥檒l add a second headline component below our Contact Form to say Thank you. We鈥檒l be in touch soon! We鈥檒l give this the opposite conditionality rule, so it鈥檚 only displayed when a user clicks submit and hidden if the state is empty.
Embedding a form
Now, our form is ready to go. The last thing we need to do is embed it somewhere. We can do this using an iFrame in HTML.
Each time we do this, we鈥檒l replace the :channel string with whatever we want to call the channel where it鈥檚 hosted - like our website, emails, or social networks.
So, if we put this on our website, the URL slug might be:
1/contact-form/website
In an email we might call it:
1/contact-form/email
Or even:
1/contact-form/sales-email-23-08-2023
You get the picture.
If we want to embed the form in an iFrame on our website, the full HTML might be:
1"<iframe width="800" height="800" frameborder="0" allow="clipboard-write;camera;geolocation" src="https://your-budibase-host/embed/crm/#/contact-form/website"> </iframe>"
You can use your own custom HTML to define the size of your embedded iFrame - or any permissions you want to provide it.
Automatic notifications
We also said that we want to notify our sales team when a new lead has submitted their details. We鈥檙e going to do this using Discord messages - but you could just as easily do it with Slack, email, or the communication channel of your choice.
We鈥檒l start by heading over to the Automation tab and hitting create automation. We鈥檒l be prompted to choose a name and a trigger.
We鈥檒l call our rule NewContact and use a row created trigger:
This means that our automation sequence will be initiated each time a row is added to whichever table we choose - in our case, contacts.
Now, we need to define what happens next. Luckily, 黑料正能量 has built-in automation actions for messaging tools like Discord and Slack. So, we鈥檒l add a Discord action:
At this point, we need to head over to a Discord server where we have admin permission and create a WebHook to post inside whichever channel we want our message to go.
For the sake of demonstration here, we鈥檒l use the following URL as a dummy webhook:
1http://our-example-webhook
We can paste this into the Discord URL field of our automation action, as well as giving our new bot a name:
Then, we can open up the bindings drawer of the message field and input whatever message we want to be sent when a user submits their details:
We鈥檝e used three bindable values from the trigger row to populate the lead鈥檚 channel, first name, and email address. So, the full message is:
1You have a new contact from {{ trigger.row.channel }}. {{ trigger.row.firstname }} is waiting to chat. Their email is {{ trigger.row.email }}.
Once you鈥檝e tested your automation and you鈥檙e satisfied that it works, you need to publish your app for it to go live.
And that鈥檚 our lead capture form completed.
You might also like to check out our in-depth guide to form UI design for some inspiration for further customization.
4. Create a new contacts screen
Next, we need to add a couple more screen that are pretty similar to our original opportunities UI. We want to replicate the same design, but basically just populate it with slightly different data.
The first of these will be for our new contacts - leads who have submitted their details, but our team hasn鈥檛 reached out to them yet.
To save a bit of time, we鈥檒l kick off by duplicating our existing opportunities screen:
We鈥檒l call this one new leads.
All of the design will be the same, we just need to reconfigure some of the components to populate them with the right data.
The first thing to do is change the filter setting on our table block so that it displays all of the rows with a status attribute that鈥檚 equal to New:
That鈥檚 the easy part.
To populate the cards with relevant data, we鈥檒l need a couple more custom queries that filter for status=New.
Let鈥檚 head back to the Data section and duplicate our AvgandTotalValue query. We鈥檒l call the new query AvgAndTotalValueOfNewLeads.
And, we鈥檒l simply edit the existing WHERE clause to filter for 蝉迟补迟耻蝉=鈥橬别飞鈥:
The full query is now:
1SELECT
2
3 SUM(EstimatedValue) AS TotalEstimatedValue,
4
5鈥 AVG(EstimatedValue) AS AvgEstimatedValue
6
7FROM
8
9 Contacts
10
11鈥 WHERE
12
13鈥 status = 'New'
Then we鈥檒l do the same thing on our Last7DaysContact query - creating a new version called Last7DaysContactNewLeads.
Again, we鈥檒l simply change the WHERE clause, so that the new query is:
1SELECT COUNT(*) AS RecentContactCount
2
3FROM Contacts
4
5WHERE LastContact >= CURRENT_DATE - INTERVAL '7 days'
6
7AND LastContact < CURRENT_DATE + INTERVAL '1 day'
8
9AND Status = 'New';
Once both of those new queries are saved, we鈥檒l head back to the Design section. Then, we need to swap out the Data field for each of the card block components to the relevant new queries - giving us:
Adding menu configuration
At this point, our internal users can access two distinct screens - so, we need some solution to enable them to navigate the app. To do this, hit the Links icon on the left-hand side and choose Configure Links.
This gives us a bindings drawer where we can choose which pages will appear in our nav menu and how they鈥檒l be displayed:
5. Create a customers screen
Next, we want to repeat this exact same process for existing customers. These are entries on our contacts table where the status is equal to Won. Rather than repeating ourselves, we鈥檒l just quickly list out the steps and then provide you with the specific queries.
So, we need to:
- Duplicate our opportunities screen. We鈥檒l call the new version /customers.
- Edit the Table Block鈥檚 filter to display rows where status=鈥漺on鈥.
- Duplicate our two custom queries and update their WHERE clauses to status=鈥漺on鈥.
- Reconfigure our card blocks鈥 Data fields to these new queries.
- Update our navigation to include the new screen.
We have two new queries to achieve this. The first is called AvgAndTotalValueOfCustomers:
1SELECT
2
3 SUM(EstimatedValue) AS TotalEstimatedValue,
4
5鈥 AVG(EstimatedValue) AS AvgEstimatedValue
6
7FROM
8
9 Contacts
10
11鈥 WHERE
12
13鈥 status = 'Won'
And the other is called Last7DaysCustomers:
1SELECT COUNT(*) AS RecentContactCount
2
3FROM Contacts
4
5WHERE LastContact >= CURRENT_DATE - INTERVAL '7 days'
6
7AND LastContact < CURRENT_DATE + INTERVAL '1 day'
8
9AND Status = 'New';
6. Add a dashboard for our home screen
Lastly, we want to create a home screen that will be based around a dashboard for displaying some top-level metrics about our customer data - giving us some helpful analytical CRM features.
We鈥檒l start by creating one last blank screen and naming it /home:
The first part of our dashboard will be a bar graph displaying the different channels that our leads are coming through.
We鈥檒l create another custom query called ChannelsForLeads. The SQL command we鈥檒l use is:
1SELECT
2
3 channel,
4
5 COUNT(*) AS ChannelCount
6
7FROM
8
9 contacts
10
11WHERE
12
13 channel is not null
14
15GROUP BY
16
17 channel
18
19ORDER BY
20
21 ChannelCount DESC;
This returns all of the channels that are assigned to different rows, alongside the number of rows that have each one.
Now, back on our home screen we鈥檒l add a Chart Block, choose the bar type, and set its Data to this new query. We鈥檒l set the label column to channel and the data column to channelcount. This gives us the following chart:
Below this, we want a pie chart that will display a breakdown of all of our current leads by their status.
Again, we鈥檙e going to need a custom query. We鈥檒l call this one StatusOfAll. The query is:
1SELECT
2
3 status,
4
5 COUNT(*) AS StatusCount
6
7FROM
8
9 contacts
10
11WHERE
12
13 status IS NOT NULL
14
15GROUP BY
16
17 status
18
19ORDER BY
20
21 StatusCount DESC;
Basically, we鈥檙e doing the same thing as before, but this time with the status attribute, rather than channel.
We鈥檒l add another Chart Block using our new query - this time choosing the Pie type. Our label column will be status and the data column is statuscount.
We also our new home screen to the menu and tick the set as home screen box so that authenticated users will land on this page when they open our app.
Here鈥檚 what the finished home screen looks like:
How to build a CRM
It鈥檚 worth restating that a huge part of the motivation for building a CRM will always be the need the replicate your own unique sales processes.
What we鈥檝e presented today is an illustration of what this could look like.
Using 黑料正能量, you have the power to adapt this however you need - adding your own data, automation rules, interfaces, integrations, plug-ins, and more. You can also choose to host your custom CRM on our cloud platform or your own infrastructure.
To learn more, take a look at our features overview .