Squishy Software Series

Adding Extensibility to Twenty, a Modern CRM

October 15, 2024
Benjamin Eckel

We started Dylibso with the thesis that all software should be extensible and that Wasm is the breakthrough that will get us there. When people asked me what I thought that would look like, Salesforce was one of the few real-world companies that I could reference.

Salesforce is at its core an end-user programmable CRM. It has a customizable data-model and is extendable using their own programming language: Apex. The powerful thing about Salesforce is that, as a customer, you can have a fully customized solution without running any infrastructure or having any programmers or operators on staff. This has made Salesforce infamously sticky and wildly successful.

However, as you can imagine, it was a long and very expensive road for them to get there. It’s a complex system which involved writing their own programming language, execution environment, and developer experience. We want XTP to enable application developers to offer a similar system, but modernized for the 2020s. Users should be able to code in the language of their choice, run and test plug-ins outside of the system, and this development should be doable quickly and cheaply.

XTP + Twenty in Action

Twenty: Open Source CRM

If we’re offering up the promise to be your own Salesforce, I thought this would be a great opportunity to try it myself. So I decided to simulate the journey of creating Salesforce, but with XTP. Salesforce is proprietary and already finished, so I decided on another strategy. I searched for a modern CRM which lacked the extensibility that Salesforce has. If I could find something open source, then I could add extensibility with XTP on my own.

This led me to Twenty. It’s a solid CRM with the core data-model architecture and a nice UI, but they haven’t added extensibility yet. From their README:

🔗 Extensibility: We’re putting the power in your hands. Soon, you’ll have the tools to extend and customize Twenty with plugins and more.

I talked to their team on Discord, and it seems they have their own plan which is still in the works (as of October 2024).

My goal was to integrate XTP into Twenty and get some non-trivial real plug-ins running in a couple days. In the end we’ll have:

  • Support for language agnostic plugins – write plugins in Python, TypeScript, Rust, Go, anything that can run as Wasm.
  • Typed bindings for all the languages  – we want to provide a quality DX with all of our types in the language of the plugin developer.
  • A Wasm sandboxed, in-process execution environment – we need isolation as well as speed and reliability. You don’t get any faster or more reliable than an in-process function call!
  • Guest accounts – authenticate plug-in developers and let them deploy code right into Twenty.

Strategy

My goal was to replicate Salesforce Triggers. This system offers a way to hook into the lifecycle event of any object in Salesforce and add some custom logic. This is the simplest pattern for extending an existing application because they all have some kind of CRUD lifecycle with the same semantics. I borrowed a few ideas from a previous exploration into extending Rails applications with Extism to achieve this.

Step 1 is getting Twenty to hand over control to our plug-in at the appropriate place. In most applications, the easiest place to do this is by connecting to the ORM (or whatever abstraction you have around your data layer). Here's a basic diagram to show how the ORM fits in:

So all we need to do is hook into every lifecycle event and pass over control to one (or many) plug-in(s). Let’s look at a single event. Say a user wants to update a Task object:

  1. ORM receives an event
  2. Call the beforeUpdateTask function on the plug-in
  3. Update the task with the change
  4. Call the afterUpdateTask function on the plug-in
  5. Respond to the user

For both of these functions, we can pass both the original object and the change. For the "before" event, the plug-in will be able to pass back an updated change object. In TypeScript, the interface will look something like this:

type TaskChange = Partial<Task>;

interface TaskEvent {
  original: Task;
  change: TaskChange;
}

function beforeTaskUpdate(event: TaskEvent): TaskChange;
function afterTaskUpdate(event: TaskEvent);

We can implement these functions to signal to the system that we wish to hook into these lifecycle events.

Granting the Plug-in Access to Twenty’s Functions

So this takes care of the trigger part, but how do we give these plug-ins the power to do interesting things? And what if the plug-in needs more context than just the task and the change data? This is where imports (host functions) come in. We can give the plug-in restricted access to call some of the functions in our host application. This can be tricky to model though. Which functions should we expose? How do we know it’s secure? How do we document and implement these?

As explored in the previously mentioned blog post, this concept of opening up a memory segregated interface into your application maps very well conceptually to an HTTP API. So we can re-use that!

Thankfully, Twenty has a REST API and an OpenAPI specification. We can re-use the REST controller code to write our imports. Instead of an opening a connection and calling an HTTP endpoint, our application can just call these controllers directly with the JSON from the plug-in (and give any JSON back). We can convert our existing OpenAPI schema into an XTP Schema to define the interface and drive the code generation and documentation.

Example Plug-in

To keep things ultra-simple. Here is an example plug-in that hooks into the beforeTaskUpdate event. If the user is trying to change the status to "DONE", we will look up their manager and assign them to the task using the `findManyWorkspaceMembers` function we created using the technique described above.

// exporting this function to signal to the system that we want
// to hook into the before-task-update event
export function beforeTaskUpdateImpl(event: TaskEvent): TaskChange {
  const change = event.change
  
  // when a user is moving the task to done
  if (change?.status == TaskStatus.DONE) {
    // find our manager by email and assign them to this task
    const managerUser = findUserByEmail("their-manager@acme.com")
    change.assigneeId = managerUser.id
  }
 
  return change
}

// utility to call host function to find another user
function findUserByEmail(email: string): WorkspaceMember {
  // find our manager's id by email using host function
  // looks like a rest call but is effectively a local function call
  // so it's 100x faster and has 100% uptime
  const memberResult = findManyWorkspaceMembers(
    {
      filter: { userEmail: { eq: email } },
      limit: 1
    }
  )
  return memberResult.workspaceMembers[0]
}

Overall, modifying Twenty to support an extension system like Saleforce Apex was a breeze. Now, we can hook into any event and the whole data model with plugins written in many different languages. Check out the full set of changes and the source to the example plugins on GitHub (we'll open a PR to Twenty too).

You can add this kind of extensibility to your app too - create a free XTP account to get started today.


Benjamin Eckel
Co-founder & CTO, Dylibso

Grow with customer use cases

Empower your users to customize your product precisely for them, while you focus on the core. A plugin system maximizes product flexibility to meet users' dynamic needs, so your product grows with your customers.

Meeting customers' needs →

Reduce churn & increase usage

Product adoption can be a double-edged sword. How do you prioritize a tidal wave of feature requests? Give users the freedom to make your product do more for them on their own timeline, and they'll stick around.

Improve customer retention →

Reclaim control of your roadmap

When customers can self-serve entire features, you can get back to building the vision. Take back valuable engineering time & innovate on your product.

On the road to roadmap freedom →

Deeper & advanced integrations

Going beyond the HTTP API, running customer code directly enables advanced use-cases compared to Webhooks & other system integration techniques.

More integration possibilities →

Pricing

Free

Create your proof-of-concept, integrating XTP into your app at no cost.

Enjoy full access to the XTP platform, limited by the number of apps, extension points, and plugins that can be created.

Full access, managing up to:

  • 1 App
  • 1 Team (up to 10 members)
  • 10 Authorized Guests (to push plugins)
  • 2 Extension Points
  • 100 Continuous plugin simulations (per month)

👀 See what you can do with free XTP:

A quick intro to XTP - safely run user code in your program
Create a free account →

Enterprise

Fully white-label your implementation, contained entirely behind your system.
‍‍
Hands-on training, enablement, and onboarding provides your company with a solid path to integration success.

Meets advanced scenarios with:

  • Custom plan, fit to your needs
  • Tailored training sessions & assistance
  • Predictable, set pricing
  • Uptime SLA + up to 24/7 direct support
  • Private cloud & object storage integration
  • Access to compliance attestations including: SOC 2 Type 2, ISO 27001, & GDPR

Talk to our team to learn more about how best to leverage XTP in your product or platform.
‍‍

Contact Sales →
Custom

Leverage next-gen technology

Use XTP’s powerful plugin execution engine to easily and securely run customer code, directly within your application. Let us handle the storage, validation, distribution and monitoring all while presenting a top-notch experience for plugin developers.

Join the Waitlist
Have questions?
Connect with us!