Blog

/

Published Monday January 20, 2025

Extensibility With Plugins

Brayden Wilmoth

Brayden Wilmoth

@BraydenWilmoth
Extensibility With Plugins

Today we are excited to announce our latest database changing feature, Plugins. When you deploy any StarbaseDB instance today you automatically gain access to core features such as REST API on top of your database, Web Sockets to execute queries, and many more developer friendly features. All of this functionality is intended to make developing on top of any database in the world easy. Each of these components are designed to be plugins themselves in the system; an optional feature that can be easily plugged or unplugged as needed.

Now you can customize and extend your database with any application-layer code you want to write, or even get started with a number of pre-built plugins ready for you today. You can think of Plugins as a way to extend your databases capabilities by adding zero latency executable code en route to your database statement executions. Perhaps you want to block unapproved queries, or respond quickly with cached results, mask data as it leaves your database, or add “nice-to-have” macros to enhance your SQL querying experience, the possibilities are limitless.

Examples available today include:

  • Stripe Subscriptions – easily support user subscriptions without hassle

  • SQL Macros – helpful syntax functions and rules to writing better queries

  • Web Sockets – execute SQL queries over secure web socket to your database

  • Outerbase Studio – a database GUI that provides an easy way to view and query your database

Plugins coming soon:

  • Change Data Capture – subscribe to events as changes are applied to your tables

  • Query Log – view a list of recent queries ran and their execution duration

  • User Authentication – support a variety of auth providers for row level security

How Plugins Work

Plugins enable you to write code that can perform tasks by adding new endpoints to your HTTP deployment or by tapping into execution flow hooks before a query is executed on your database, or even after the transaction on the database has completed. They provide an immense amount of flexibility when it makes sense for application logic to be coupled with your data. Below we will walk through a few examples of how plugins can be used.

Available Overrides

To get the most out of plugins there are function overrides available for you to have access to events as they are happening such as:

  • When the plugin is being registered

  • When a SQL statement is about to be executed

  • When a SQL statement has concluded with a response

And being able to tap into those moments are what make plugins so valuable. As you will learn by examples that follow, these are the functions you can override within a StarbasePlugin class that give you all the power you need.

Plugin Registered (Initialized)

override async register(app: StarbaseApp) { }

Pre-Query Hook

Returns sql and params either as they were passed in, or mutated by code.

override async beforeQuery(opts: { sql: string; params?: unknown[]; dataSource?: DataSource; config?: StarbaseDBConfiguration }): Promise<{ sql: string; params?: unknown[] }> { }

Post-Query Hook

Returns a result object either as it was passed in, or mutated by code.

override async afterQuery(opts: { sql: string; result: any; isRaw: boolean; dataSource?: DataSource; config?: StarbaseDBConfiguration; }): Promise<any> { }

Adding HTTP Endpoints

Below let’s explore an example where we add a new HTTP endpoint to our StarbaseDB instance dynamically to return a simple response.

class ReachableStatusPlugin extends StarbasePlugin {
    constructor() {
        super('starbasedb:reachable-status')
    }

    override async register(app: StarbaseApp) {
        // Add a new route to our Hono instance
        app.get('/reachable', (c) => {
            return this.handle(c)
        })
    }

    private handle(ctx: StarbaseContext): Response {
        return createResponse({
            reachable: true
        }, undefined, 200)
    }
}

Above is all that is required to make a plugin functional. In this example we are overriding the register(...) call to insert a new Hono route into our existing global application object. Once this plugin is registered our new endpoint /reachable can be accessed and we should see the response we expect from it when we make an authenticated network call to it.

To register the plugin we first must include it in our plugins array in our ./src/index.ts file. Adding the instantiation of this class into the plugins array tells Starbase that it needs to call the register call on the plugin as the Worker instance is spinning up. Below is what that might look like.

const plugins = [
    new WebSocketPlugin(),
    new StudioPlugin({
        username: env.STUDIO_USER,
        password: env.STUDIO_PASS,
        apiKey: env.ADMIN_AUTHORIZATION_TOKEN,
    }),
    new ReachableStatusPlugin() // <-- Our newly inserted plugin
] satisfies StarbasePlugin[]

Pre-Query Modifications

You can listen to incoming SQL execution requests to observe, modify, or throw an error before the operation is performed.

In this example we want to examine the incoming SQL statement and evaluate if the user is attempting to perform a SELECT * query. Usually the use of an asterisk to get all columns is less than ideal and we should encourage our users to explicitly state which columns of data they intend to use. While this example is merely for demonstrative purposes (as you’ll see in the code) we have a SQL Macros plugin where you can evaluate a more production-ready version ready to be used as a plugin today with the same functionality (see SQL Macros Plugin).

What we will do in our example is create a plugin that overrides the beforeQuery hook. When a SQL query comes through, each plugin that overrides the beforeQuery function will have an opportunity to perform operations that may read or alter the sql and params values of the statement or decide to throw an error altogether to prevent the request from proceeding. In our example we are going to do a simple string check to see if the substring SELECT * exists and if it does throw an error to abort the operation early. Let’s take a look at how this might be implemented.

export class PreventSelectStarPlugin extends StarbasePlugin {
    constructor() {
        super('starbasedb:prevent-select-star')
    }

    override async beforeQuery(opts: { sql: string; params?: unknown[]; dataSource?: DataSource; config?: StarbaseDBConfiguration }): Promise<{ sql: string; params?: unknown[] }> {
        let { dataSource, sql, params } = opts;

        if (sql.toLowerCase().includes('select *')) {
            throw new Error('SELECT * is not allowed. Please specify explicit columns.');
        }

        return Promise.resolve({
            sql,
            params
        }); 
    }
}

Post-Query Modifications

You can also listen to results of performed SQL executions prior to them being returned back to the caller. Similar to the pre-query hook you can observe the results, modify the results, or throw an error before the result is returned to the caller.

In the below example we want to make sure that any column with the name password never is included in a response back to the caller. Again, this example is not production-ready code but instead used to demonstrate how you could manipulate the query results before responding back to the user.

What our code will do is override the afterQuery event hook that all plugins may optionally override for us to perform some custom logic. We will then observe what the data results are by scanning over each of the objects and see if a key named password exists and if so we will strip it from each object that contains it. Then we pass back our altered results in the response which the next plugin that overrides afterQuery will have access to our results object or if no other plugin exists to be called afterwards that will be the final result returned to the caller.

export class PreventPasswordPlugin extends StarbasePlugin {
    constructor() {
        super('starbasedb:prevent-password')
    }

    override async afterQuery(opts: { sql: string; result: any; isRaw: boolean; dataSource?: DataSource; config?: StarbaseDBConfiguration; }): Promise<any> {
        let { result } = opts;

        if (Array.isArray(result)) {
            result = result.map(obj => {
                const filtered = { ...obj };
                delete filtered.password;
                return filtered;
            });
        }

        return Promise.resolve(result);
    }
}

Skip Authentication

Some use cases exist where we want to return a response to a user without requiring authentication. Maybe we want to handle the authentication ourselves, similar to how we do in the Outerbase Studio plugin example. There is a way for us to tell our plugins to respond to the callers request without verifying they are authenticated by one of our three supported authenticated checks, which include:

  • Contains an Authorization header value of our ADMIN_AUTHORIZATION_TOKEN

  • Contains an Authorization header value of our CLIENT_AUTHORIZATION_TOKEN

  • Contains an Authorization header value of a users JWT

Assuming none of those three need to be true for a particular endpoint we can mark our plugin to not require auth by setting that value in our constructor.

class ReachableStatusPlugin extends StarbasePlugin {
    constructor() {
        super('starbasedb:reachable-status', {
            requiresAuth: false
        })
    }

    override async register(app: StarbaseApp) {
        // Add a new route to our Hono instance
        app.get('/reachable', (c) => {
            return this.handle(c)
        })
    }

    private handle(ctx: StarbaseContext): Response {
        return createResponse({
            reachable: true
        }, undefined, 200)
    }
}

By default, authentication is required for all plugins unless explicitly set to be false for security reasons. Our approach provides enough flexibility for users to control how items are accessed and with what type of privileges, allowing for us to ensure the creation of secure yet customizable applications that cater to everyones needs in a simplified way.

Conclusion

Plugins can be written in only a handful lines of code making them quick and easy to get started with all while supporting incredibly complex use cases. We like to think of them as versions of microservices that specialize in a very specific task but easy to “attach” to your database and get started in minutes. Imagine a world where you piece together Payments, OAuth, Email and Edge Caching in 4 lines of code. That’s the reality we are bringing to life.

Using Cloudflare Workers, there is zero latency added to your response time due to the nature of network routing and how they prepare your code to be ran as the request is forming between client and server. As your network request would normally be routed through the cords of the internet, the Worker serves as just one of potentially many hops that might occur en route to the destination. In some cases simply by traveling through the Cloudflare network you may see faster response times with their state of the art network routing.

What will you build?

Space, at your fingertips
astronaut

What will you discover?