Engineering

Part I: How We Built An Internal Tools Builder That Lives In Your Python Codebase

Dropbase is a local-first internal tools builder for Python developers. It integrates with any Python codebase and supports custom packages. Call server-side functions, reuse ORM models, import PyTorch or Pandas. In this post, we talk in detail about how we built this framework.

This post is published in 2 parts:

  1. Part I: Why we built it?
  2. Part II: How we built it?

Why we built it?

Internal tools builders gave developers a step up in productivity by making it easy to build custom tools for internal use. These tools typically consist of UI components that call API endpoints in your backend, database, or app servers. While the approach has general applicability, it isn't a great option for developers who need to import use-case specific packages or who have written important code that is not already exposed as API endpoints.

In fact, to do this with internal tool builders, devs have 3 choices: expose their functions as API endpoints, front their existing codebase with an RPC server, or rewrite/duplicate backend logic into their internal tools builder.

Why are these options a problem?

The simple answer is that it requires more work — more resources, effort, and redundant code. For context, most internal tools work the same way: UI components in the client trigger/call backend endpoints to perform actions or process data. However, in polyglot stacks (e.g. internal tool client in Javascript; backend server in Python), devs are limited to calling backend endpoints via REST APIs (or GraphQL). They cannot directly call server-side functions or reuse existing ORM models without more effort.

To work around this limitation, devs increasingly expose more functions as endpoints, even those that they shouldn’t/can’t due to risks and compliance reasons. Another workaround is to set up an RPC server in front of their backend that allows the internal tool builder’s client to call their server-side functions directly. The latter approach requires setting up/maintaining an RPC server and registering each function before using them, leading to additional dev work. A third and common workaround is to rebuild the existing logic of their server-side functions within the tool builder itself. In this approach, changes in the backend logic may need to be written twice: once in their backends and again in their internal tool builder.

The result of these workarounds are compromises: security risks, additional overhead (server and dev effort), and redundant logic living in multiple locations. Furthermore, internal tool builders often do not make their underlying code easily accessible, so devs can’t version control it. And even if they could do it for the individual code snippets, it’s not version control in the full sense due to the differences in languages and architectures: there’s very little devs can do with the code written in the tool builder’s framework. Code sprawls and it is difficult to keep changes made in the backend synced with those inside the internal tool builder.

Is there a solution?

There’s at least one that doesn’t involve building from scratch: developers could bring the internal tool builder closer to their codebase. How close is “close”? How about one that’s in the same filesystem. We’ll explore this solution in this post. 

To set context for this solution, picture an internal tool-building framework that lives in your codebase. It would allow devs to reuse any existing server-side function, install custom packages, reuse ORM models, or import helpers/utilities just like they would if they were building it from scratch. Instead of hunting for UI component libraries and wiring them up to server-side functions, they could just declare them as needed and easily pass data between UI and functions. Previously inaccessible backend logic could now be callable, alongside those already exposed and accessible via API endpoints; UI components could directly make function calls to logic in the backend when applicable.

For example, when an end user clicks on the “Update” button, the Python function below is directly executed

User input entered in the widget is passed as an argument to the function via an object called “state”, which we will describe later. Here’s the function that is directly bound to the button click event

If this starts to sound like a package installed in your codebase that lets you quickly build internal tools, it’s because it practically is. In this post, we’ll describe how it works and break it down and explain each of its core components.

To learn more about how we built it, continue to ready Part II: How we built it?

Newsletter
Insights and updates from the Dropbase team.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
By signing up you agree to our Terms of Service