Building ChatGPT Plugins with Supabase Edge Runtime

2023-05-15

10 minute read

ChatGPT Plugins support is rolling out in beta this week! To help you get up and running quickly, we're releasing a plugin template written in TypeScript and running on Supabase Edge Runtime!

Want to get started right away? Fork the template on GitHub!

Serving the manifest file

The ai-plugin.json manifest file is required for ChatGPT to identify our plugin, know what kind of authentication mechanism is used, understand where to find the OpenAPI definition, and some other details about our plugin. You can find the full list of supported parameters in the OpenAI docs.

Supabase Edge Runtime does currently not support hosting/serving of static files, however, we can import JSON files in our function and serve them as a JSON response. As this needs to be at the root of our domain, we add this to our main function handler:


_11
// functions/main/index.ts
_11
import aiPlugins from './ai-plugins.json' assert { type: 'json' }
_11
_11
// [...]
_11
_11
// Serve /.well-known/ai-plugin.json
_11
if (service_name === '.well-known') {
_11
return new Response(JSON.stringify(aiPlugins), {
_11
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
_11
})
_11
}

Now, when running Edge Runtime locally via Docker, our plugin manifest will be available at http://localhost:8000/.well-known/ai-plugin.json

Generating the OpenAPI definition with swagger-jsdoc

The OpenAPI definition is required for ChatGPT to know how to underact with our API. Only endpoints included in there will be exposed to ChatGPT, which allows you to selectively make our endpoints available, or add specific endpoints for ChatGPT.

The OpenAPI definition can be either in YAML or JSON format. We’ll be using JSON and the same approach as above to serve it. Writing an OpenAPI definition is not something we will want to do by hand, luckily there is an open source tool called swagger-jsdoc which we can use to annotate our endpoints with JSDoc comments and generate the OpenAPI definition with a little script.


_22
// /scripts/generate-openapi-spec.ts
_22
import swaggerJsdoc from 'npm:swagger-jsdoc@6.2.8'
_22
_22
const options = {
_22
definition: {
_22
openapi: '3.0.1',
_22
info: {
_22
title: 'TODO Plugin',
_22
description: `A plugin that allows the user to create and manage a TODO list using ChatGPT. If you do not know the user's username, ask them first before making queries to the plugin. Otherwise, use the username "global".`,
_22
version: '1.0.0',
_22
},
_22
servers: [{ url: 'http://localhost:8000' }],
_22
},
_22
apis: ['./functions/chatgpt-plugin/index.ts'], // files containing annotations as above
_22
}
_22
_22
const openapiSpecification = swaggerJsdoc(options)
_22
const openapiString = JSON.stringify(openapiSpecification, null, 2)
_22
const encoder = new TextEncoder()
_22
const data = encoder.encode(openapiString)
_22
await Deno.writeFile('./functions/chatgpt-plugin/openapi.json', data)
_22
console.log(openapiString)

Since this script is run outside of the function execution, e.g. as a GitHub Action, we can use npm specifiers to import swagger-jsdoc.

Next, we create our /functions/chatgpt-plugin/index.ts file where we use the Deno oak router to build our API and annotate it with JSDOC comments.


_64
// /functions/chatgpt-plugin/index.ts
_64
import { Application, Router } from 'https://deno.land/x/oak@v11.1.0/mod.ts'
_64
import openapi from './openapi.json' assert { type: 'json' }
_64
_64
console.log('Hello from `chatgpt-plugin` Function!')
_64
_64
const _TODOS: { [key: string]: Array<string> } = {
_64
user: ['Build your own ChatGPT Plugin!'],
_64
}
_64
_64
/**
_64
* @openapi
_64
* components:
_64
* schemas:
_64
* getTodosResponse:
_64
* type: object
_64
* properties:
_64
* todos:
_64
* type: array
_64
* items:
_64
* type: string
_64
* description: The list of todos.
_64
*/
_64
_64
const router = new Router()
_64
router
_64
.get('/chatgpt-plugin', (ctx) => {
_64
ctx.response.body = 'Building ChatGPT plugins with Deno!'
_64
})
_64
/**
_64
* @openapi
_64
* /chatgpt-plugin/todos/{username}:
_64
* get:
_64
* operationId: getTodos
_64
* summary: Get the list of todos
_64
* parameters:
_64
* - in: path
_64
* name: username
_64
* schema:
_64
* type: string
_64
* required: true
_64
* description: The name of the user.
_64
* responses:
_64
* 200:
_64
* description: OK
_64
* content:
_64
* application/json:
_64
* schema:
_64
* $ref: '#/components/schemas/getTodosResponse'
_64
*/
_64
.get('/chatgpt-plugin/todos/:username', (ctx) => {
_64
const username = ctx.params.username.toLowerCase()
_64
ctx.response.body = _TODOS[username] ?? []
_64
})
_64
.get('/chatgpt-plugin/openapi.json', (ctx) => {
_64
ctx.response.body = JSON.stringify(openapi)
_64
ctx.response.headers.set('Content-Type', 'application/json')
_64
})
_64
_64
const app = new Application()
_64
app.use(router.routes())
_64
app.use(router.allowedMethods())
_64
_64
await app.listen({ port: 8000 })

With our JSDoc annotation in place, we can now run the generation script in the terminal:


_10
deno run --allow-read --allow-write scripts/generate-openapi-spec.ts

Adding the CORS headers

Lastly, we need to add some CORS headers to make the browser happy. We define them in a /functions/_shared/cors.ts file so we can easily reuse them across our main and chatgpt-plugins function.


_10
// /functions/_shared/cors.ts
_10
export const corsHeaders = {
_10
'Access-Control-Allow-Origin': 'https://chat.openai.com',
_10
'Access-Control-Allow-Credentials': 'true',
_10
'Access-Control-Allow-Private-Network': 'true',
_10
'Access-Control-Allow-Headers': '*',
_10
}

Now we can easily add them to all our chatgpt-plugin routes a middleware for our oak application.


_18
// /functions/chatgpt-plugin/index.ts
_18
import { Application, Router } from 'https://deno.land/x/oak@v11.1.0/mod.ts'
_18
import { corsHeaders } from '../_shared/cors.ts'
_18
_18
// [...]
_18
const app = new Application()
_18
// ChatGPT specific CORS headers
_18
app.use(async (ctx, next) => {
_18
await next()
_18
let key: keyof typeof corsHeaders
_18
for (key in corsHeaders) {
_18
ctx.response.headers.set(key, corsHeaders[key])
_18
}
_18
})
_18
app.use(router.routes())
_18
app.use(router.allowedMethods())
_18
_18
await app.listen({ port: 8000 })

Running locally with Docker

Now that we’ve got all the pieces in place, let’s spin up Edge Runtime locally and test things out. For this, we need a Dockerfile and for convenience, we can add a docker-compose file also.


_10
// Dockerfile
_10
FROM ghcr.io/supabase/edge-runtime:v1.2.18
_10
_10
COPY ./functions /home/deno/functions
_10
CMD [ "start", "--main-service", "/home/deno/functions/main" ]

This will pull down Edge Runtime v1.2.18 (you can check the latest release here) and start up the main service (our /functions/main/index.ts function).


_11
// docker-compose.yml
_11
version: "3.9"
_11
services:
_11
web:
_11
build: .
_11
volumes:
_11
- type: bind
_11
source: ./functions
_11
target: /home/deno/functions
_11
ports:
_11
- "8000:9000"

Edge Runtime will serve requests on port 9000, so we’re creating a mapping from [localhost:8000](http://localhost:8000) where we want to serve our requests locally (of course you can adapt this to your needs) to port 9000 of our Docker container.

Furthermore, we’re using bind mounts to mount our functions directory into the container. This allows us to make modifications to our functions without needing to rebuild the container after, making for a great local developer experience.

That’s it, now we can build and spin up our container from the terminal:


_10
docker compose up --build

Go ahead and try it out by visiting:

Installing and testing the plugin locally

You can conveniently test your plugin while running it on localhost using the ChatGPT UI:

  1. Select the plugin model from the top drop down, then select “Plugins”, “Plugin Store”, and finally “Develop your own plugin”.
  2. Enter localhost:8000 and click "Find manifest file".
  3. Confirm with “Install localhost plugin”.

That’s it, now go ahead and ask some questions, e.g. you can start with “Do I have any todos?”

There you are, now go ahead and build your own plugin as it says on your todo list ;)

Deploying to Fly.io

Once you’re happy with the functionality of your plugin, go ahead and deploy it to Fly.io. After installing the flyctl cli, it only takes a couple of steps:

  • Change http://localhost:8000 to your Fly domain in the /main/ai-plugins.json file
  • Open fly.toml and update the app name and optionally the region etc.
  • In your terminal, run fly apps create and specify the app name you just set in your fly.toml file.
  • Finally, run fly deploy.

There you go, now you’re ready to release your plugin to the world \o/

Conclusion

ChatGPT is a powerful new interface and its usage is growing rapidly. With ChatGPT Plugins you can allow your users to access your service directly from ChatGPT, using cutting edge technologies like TypeScript and Deno.

In a next step you can add authentication to your plugin, let us know on Twitter if you’d be interested in a tutorial for that. We can’t wait to see what you will build!

More AI resources

Share this article

Build in a weekend, scale to millions