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:
- ORM receives an event
- Call the
beforeUpdateTask
function on the plug-in - Update the task with the change
- Call the
afterUpdateTask
function on the plug-in - 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