Skip to content

src backend howto_make_driver

KernelDeimos edited this page Feb 25, 2025 · 1 revision

How to Make a Puter Driver

What is a Driver?

A driver can be one of two things depending on what you're talking about:

  • a driver interface describes a general type of service and what its parameters and result look like. For example, puter-chat-completion is a driver interface for AI Chat services, and it specifies that any service on Puter for AI Chat needs a method called complete that accepts a JSON parameter called messages.
  • a driver implementation exists when a Service on Puter implements a trait with the same name as a driver interface.

Part 1: Choose or Create a Driver Interface

Available driver interfaces exist at this location in the repo: /src/backend/src/services/drivers/interfaces.js.

When creating a new Puter driver implementation, you should check this file to see if there's an appropriate interface. We're going to make a driver that returns greeting strings, so we can use the existing hello-world interface. If there wasn't an existing interface, it would need to be created. Let's break down this interface:

'hello-world': {
    description: 'A simple driver that returns a greeting.',
    methods: {
        greet: {
            description: 'Returns a greeting.',
            parameters: {
                subject: {
                    type: 'string',
                    optional: true,
                },
            },
            result: { type: 'string' },
        }
    }
},

The description describes what the interface is for. This should be provided that both driver developers and users can quickly identify what types of services should use it.

The methods object should have at least one entry, but it may have more. The key of each entry is the name of a method; in here we see greet. Each method also has a description, a parameters object, and a result object.

The parameters object has an entry for each parameter that may be passed to the method. Each entry is an object with a type property specifying what values are allowed, and possibly an optional: true entry.

All methods for Puter drivers use named parameters. There are no positional parameters in Puter driver methods.

The result object specifies the type of the result. A service called DriverService will use this to determine the response format and headers of the response.

Part 2: Create a Service

Creating a service is very easy, provided the service doesn't do anything. Simply add a class to src/backend/src/services or into the module of your choice (src/backend/src/modules/<module name>) that looks like this:

const BaseService = require('./BaseService')
// NOTE: the path specified ^ HERE might be different depending
//       on the location of your file.

class PrankGreetService extends BaseService {
}

Notice I called the service "PrankGreet". This is a good service name because you already know what the service is likely to implement: this service generates a greeting, but it is a greeting that intends to play a prank on whoever is beeing greeted.

Then, register the service into a module. If you put the service under src/backend/src/services, then it goes in CoreModule somewhere near the end of the install() method. Otherwise, it will go in the *Module.js file in the module where you placed your service.

The code to register the service is two lines of code that will look something like this:

const { PrankGreetServie } = require('./path/to/PrankGreetServie.js');
services.registerService('prank-greet', PrankGreetServie);

Part 3: Verify that the Service is Registered

It's always a good idea to verify that the service is loaded when starting Puter. Otherwise, you might spend time trying to determine why your code doesn't work, when in fact it's not running at all to begin with.

To do this, we'll add an _init handler to the service that logs a message after a few seconds. We wait a few seconds so that any log noise from boot won't bury our message.

class PrankGreetService extends BaseService {
    async _init () {
        // Wait for 5 seconds
        await new Promise(rslv => setTimeout(rslv), 5000);

        // Display a log message
        this.log.noticeme('Hello from PrankGreetService!');
    }
}

Typically you'll use this.log.info('some message') in your logs as opposed to this.log.noticeme(...), but the noticeme log level is helpful when debugging.

Part 4: Implement the Driver Interface in your Service

Now that it has been verified that the service is loaded, we can start implementing the driver interface we chose eralier.

class PrankGreetService extends BaseService {
    async _init () {
        // ... same as before
    }

    // Now we add this:
    static IMPLEMENTS = {
        ['hello-world']: {
            async greet ({ subject }) {
                if ( subject ) {
                    return `Hello ${subject}, tell me about updog!`;
                }
                return `Hello, tell me about updog!`;
            }
        }
    }
}

Part 5: Test the Driver Implementation

We have now created the prank-greet implementation of hello-world. Let's make a request in the browser to check it out. The example below is a fetch call using http://api.puter.localhost:4100 as the API origin, which is the default when you're running Puter's backend locally.

Also, in this request I refer to puter.authToken. If you run this snippet in the Dev Tools window of your browser from a tab with Puter open (your local Puter, to be precise), this should contain the current value for your auth token.

await (await fetch("http://api.puter.localhost:4100/drivers/call", {
    "headers": {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${puter.authToken}`,
    },
    "body": JSON.stringify({
        interface: 'hello-world',
        service: 'prank-greet',
        method: 'greet',
        args: {
            subject: 'World',
        },
    }),
    "method": "POST",
})).json();

You might see a permissions error! Don't worry, this is expected; in the next step we'll add the required permissions.

Part 6: Permissions

In the previous step, you will only have gotten a successful response if you're logged in as the admin user. If you're logged in as another user you won't have access to the service's driver implementations be default.

To grant permission for all users, update hardcoded-permissions.js.

First, look for the constant hardcoded_user_group_permissions. Whereever you see an entry for service:hello-world:ii:hello-world, add the corresponding entry for your service, which will be called

service:prank-greet:ii:hello-world

To help you remember the permission string, its helpful to know that ii in the string stands for "invoke interface". i.e. the scope of the permission is under service:prank-greet (the prank-greet service) and we want permission to invoke the interface hello-world on that service.

You'll notice each entry in hardcoded_user_group_permissions has a value determined by a call to the utility function policy_perm(...). The policy called user.es is a permissive policy for storage drivers, and we can re-purpose it for our greeting implementor.

The policy of a permission determines behavior like rate limiting. This is an advanced topic that is not covered in this guide.

If you want apps to be able to access the driver implementation without explicit permission from a user, you will need to also register it in the default_implicit_user_app_permissions constant. Additionally, you can use the implicit_user_app_permissions constant to grant implicit permission to the builtin Puter apps only.

Permissions to implementations on services can also be granted at runtime to a user or group of users using the permissions API. This is beyond the scope of this guide.

Part 7: Verify Successful Response

If all went well, you should see the response in your console when you try the request from Part 5. Try logging into a user other than admin to verify permisison is granted.

"Hello World, tell me about updog!"
Clone this wiki locally