⚡Supacharge your backend with Supabase & Trigger.dev
Tags: opensource, typescript, nextjs, serverless
Published: 2023-08-10 17:00:00

A big part of our inspiration when starting [Trigger.dev](https://trigger.dev) was the experience of using [Firestore Cloud Functions](https://firebase.google.com/docs/firestore/extend-with-functions) to build some complex backend orchestration tasks built on top of triggering functions whenever a document was created, updated, or deleted:

```ts
import functions from "firebase-functions";

export const myFunction = functions.firestore
  .document("todos/{todoId}")
  .onWrite((change, context) => {
    /* ... */
  });
```

This powerful primitive allowed us to build a lot of functionality very quickly. But since then we've left the world of closed-source, locked-in platforms behind: Trigger.dev is an [open-source](https://github.com/triggerdotdev/trigger.dev), [self-hostable](https://trigger.dev/docs/documentation/guides/self-hosting) project.

So is Supabase. How cool would it be if this was possible:

```ts
client.defineJob({
  id: "my-job",
  name: "My Job",
  trigger: supabase.onInserted({ table: "todos" }),
  run: async (payload, io) => {},
});
```

## Introducing @trigger.dev/supabase

Well, we're happy to announce that it is with our new [Supabase integration](https://trigger.dev/docs/integrations/apis/supabase/introduction). We've added support for Triggering jobs whenever a row is inserted, updated, or deleted in a Supabase project. And you aren't limited to just your own tables.

The Supabase integration supports specifying a schema as well as the table name. This means you can listen to changes in the `auth` schema, like when a user is created:

```ts
client.defineJob({
  id: "my-job",
  name: "My Job",
  trigger: supabase.onInserted({
    schema: "auth",
    table: "users",
  }),
  run: async (payload, io) => {},
});
```

Or trigger on a user update and check to see if their email was confirmed by filtering on the `old_record`:

```ts
client.defineJob({
  id: "my-job",
  name: "My Job",
  // Trigger when a user confirms their email
  trigger: supabase.onUpdated({
    schema: "auth",
    table: "users",
    filter: {
      old_record: {
        email_confirmed_at: [{ $isNull: true }],
      },
      record: {
        email_confirmed_at: [{ $isNull: false }],
      },
    },
  }),
  run: async (payload, io) => {},
});
```

Want to react to new file uploads to Supabase storage? No problem:

```ts
client.defineJob({
  id: "my-job",
  name: "My Job",
  trigger: supabase.onInserted({
    schema: "storage",
    table: "objects",
  }),
  run: async (payload, io) => {},
});
```

Only trigger on new PNG files uploaded to a specific bucket, under a specific path? Easy:

```ts
client.defineJob({
  id: "my-job",
  name: "My Job",
  trigger: supabase.onInserted({
    schema: "storage",
    table: "objects",
    trigger: triggers.onInserted({
      schema: "storage",
      table: "objects",
      filter: {
        record: {
          bucket_id: ["uploads"],
          name: [
            {
              $endsWith: ".png",
            },
          ],
          path_tokens: [
            {
              $includes: "images",
            },
          ],
        },
      },
    }),
  }),
  run: async (payload, io) => {},
});
```

And those are just a few examples. Here's a few other things you can do with the Supabase integration:

- Get notified when a user updates their password
- Automatically email customers when their orders are updated or shipped
- Sync data with external services or APIs when certain database fields are updated
- Send push notifications to mobile devices when new messages are received
- Adjust prices on Shopify based on changes to product costs or availability
- Sync customer subscriptions in Stripe when a Supabase user updates
- Automatically add new users to a mailing list in Loops.so
- Sync data from Supabase tables to Google Sheets for collaboration or reporting
- Send SMS notifications to users based on specific events or updates in Supabase
- Send a notification to Slack whenever a user requests a password change
- Sync uploaded files to Cloudflare R2 for backup

## Background jobs with Next.js and Supabase

Let's step through an example of how you might use this integration to build a background job that sends a few welcome emails when a user confirms their email address.

**NOTE**

_This example is based on our [Supabase Onboarding Emails example](https://github.com/triggerdotdev/examples/tree/main/supabase-onboarding-emails), which you can clone and run locally to try it out._

## Prepare Supabase & Next.js

Create a new project in Supabase and then head over to the SQL Editor (with a fancy new [AI query builder](https://supabase.com/blog/supabase-studio-3-0)) and choose the "User Management Starter" Quickstart:


![User management starter](https://trigger.dev/blog/supabase-integration/user-management-starter.png)

Then, create a new Next.js app with Supabase support:

```ts
npx create-next-app@latest -e with-supabase supabase-onboarding-emails
cd supabase-onboarding-emails
```

Rename the .env.local.example file to .env.local and add your Supabase URL and public key:

```
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```

Finally, we'll go ahead and [Generate the Typescript types](https://supabase.com/docs/reference/javascript/typescript-support) now as we'll be using them in a bit:

```ts
npx supabase gen types typescript --project-id <your project id> --schema public --schema auth > supabase-types.ts
```

Next, we'll initialize Trigger.dev in the project (you'll need to [sign up](https://cloud.trigger.dev/) for an account first and create your first project)

```ts
npx @trigger.dev/cli@latest init
```

Edit the '`middleware.ts`' file and add the following code to exclude the Trigger.dev endpoint from the Supabase auth middleware:

```ts
// Match all routes other than /api/trigger
export const config = {
  matcher: ["/((?!api/trigger).*)"],
};
```
## Add Supabase & Resend integrations

Add the `@trigger.dev/supabase` package to your project (along with `@trigger.dev/resend` for email sending)

```ts
npm add @trigger.dev/supabase @trigger.dev/resend
```

Add your [Resend.com](https://resend.com/) API key to the `.env.local` file:

```
RESEND_API_KEY=your-api-key
```

Edit the `jobs/examples.ts` file and replace with the following code:

``` ts
import { client } from "@/trigger";
import { Database } from "@/supabase-types";
import { SupabaseManagement } from "@trigger.dev/supabase";
import { Resend } from "@trigger.dev/resend";

// Use OAuth to authenticate with Supabase Management API
const supabaseManagement = new SupabaseManagement({
  id: "supabase-management",
});

// Use the types we generated earlier
const db = supabaseManagement.db<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!
);

const resend = new Resend({
  id: "resend",
  apiKey: process.env.RESEND_API_KEY!,
});

client.defineJob({
  id: "welcome-email-campaign",
  name: "Welcome Email Campaign",
  version: "1.0.0",
  trigger: db.onUpdated({
    // Trigger this job whenever a user is confirmed
    schema: "auth",
    table: "users",
    filter: {
      old_record: {
        email_confirmed_at: [{ $isNull: true }],
      },
      record: {
        email_confirmed_at: [{ $isNull: false }],
      },
    },
  }),
  integrations: {
    resend,
  },
  run: async (payload, io, ctx) => {
    // payload.record is typed based on the Supabase schema
    if (!payload.record.email) {
      return;
    }

    await io.resend.sendEmail("email-1", {
      to: payload.record.email,
      subject: `Thanks for joining Acme Inc`,
      text: `Hi there, welcome to our community! This is the first email we send you to help you get started.`,
      from: "eric@acme.com",
    });

    await io.wait("⌛", 60); // Waits for 1 minute but you might want to wait longer

    await io.resend.sendEmail("email-2", {
      to: payload.record.email,
      subject: `Here are some tips to get started`,
      text: `Hi there, welcome to our community! This is the second email we send you to help you get started.`,
      from: "eric@acme.com",
    });
  },
});
```

## Authenticate to the Supabase Management API

The Supabase Triggers use the [Supabase Management API](https://trigger.dev/docs/integrations/apis/supabase/management) to register the triggers in your Supabase projects.

You can authenticate using a Personal Access Token or via the new Supabase Management API OAuth implementation, which we are using in this example.

Login to Trigger.dev and navigate to the project "Integrations" page. Select the "Supabase Management" integration and configure it like so:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yb3clzkzuspjqr1gsa38.png)

Authorize access to your Supabase project and then you'll be ready to run the job 🚀

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d6shac1z1hjt1d6z45ll.png)

## Run and test the job

Now you are ready to run the Next.js app and test the job. Run the following command to start the Next.js app:

```
npm run dev
```

And then in a separate terminal, run the following command to start the Trigger.dev agent:

```
npx @trigger.dev/cli dev
```

Head back to your Supabase Dashboard -> Auth, and create a new user (keep "Auto Confirm User?" checked).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yxrsvzuh7sppg9llnzj.png)

Then navigate over to your Trigger.dev project dashboard and you should see the job running ✨

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sj47kc3dydjwwon552y1.png)

## How it works

Trigger.dev supports triggering jobs through [Webhooks](https://trigger.dev/docs/documentation/concepts/triggers/webhooks), which integrations can expose via Webhook Triggers. For example, this GitHub trigger will register a webhook for the `issue` event on the `triggerdotdev/trigger.dev` repository behind the scenes:

```ts
import { Github } from "@trigger.dev/github";

const github = new Github({
  id: "github",
  token: process.env.GITHUB_TOKEN!,
});

client.defineJob({
  id: "github-integration-on-issue",
  name: "GitHub Integration - On Issue",
  version: "0.1.0",
  trigger: github.triggers.repo({
    event: events.onIssue,
    owner: "triggerdotdev",
    repo: "trigger.dev",
  }),
  run: async (payload, io, ctx) => {
    //do stuff
  },
});
```

Internally, Trigger.dev provides a URL and a secret to the Github integration which uses it to register a webhook via the [Github REST API](https://docs.github.com/en/free-pro-team@latest/rest/webhooks/repos?apiVersion=2022-11-28#create-a-repository-webhook).

This is exactly how the Supabase Integration works, except it uses [Supabase Database Webhooks](https://supabase.com/docs/guides/database/webhooks), which allow you to make an HTTP request whenever there is a change in a Supabase table.

We run the following pseudo-SQL to create a webhook for the `todos` table in the `public` schema:

```ts
CREATE OR REPLACE TRIGGER triggername
AFTER UPDATE on "public"."todos"
FOR EACH ROW
  EXECUTE FUNCTION supabase_functions.http_request('https://api.trigger.dev/api/v1/sources/http/abc123', 'POST', '{"Content-type":"application/json", "Authorization": "Bearer ${secret}" }', '{}', '1000')
```

The `secret` is a secret string that is generated by Trigger.dev when a job uses a Supabase trigger. It is used to verify that the webhook request is coming from Supabase.

Here's a high-level overview of the flow for registering a Supabase trigger:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0lxchto96emk1d10uk4r.png)


**NOTE**

_We try to limit how many database webhooks we create. For example, if you have 10 jobs that trigger off the same table, we will only create two webhooks (1 for each Trigger.dev environment, `prod` and `dev`)_


## Automate your Supabase account

We've also added full support to use the new Supabase Management API inside your jobs to automate your Supabase account, allowing you to create and manage Supabase projects, databases, and more.

For example, you could create a job that automatically creates a new Supabase project and database for each new user that signs up to your app (you might want to charge for this feature 😉)

```ts
import { client } from "./trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { SupabaseManagement } from "@trigger.dev/supabase";

const supabaseManagement = new SupabaseManagement({
  id: "supabase-management",
});

client.defineJob({
  id: "create-supbase-project",
  name: "Create Supabase Project",
  version: "0.1.0",
  trigger: eventTrigger({ name: "user.created" }),
  integrations: {
    supabaseManagement,
  },
  run: async (payload, io) => {
    await io.supabaseManagement.createProject("🚀", {
      db_pass: payload.password,
      name: payload.name,
      organization_id: process.env.SUPABASE_ORGANIZATION_ID,
      plan: "pro",
      region: "us-east-1",
      kps_enabled: true,
    });
  },
});
```

For more information on what can be done in your Supabase Account, check out the [Supabase Management API reference](https://supabase.com/docs/reference/api/introduction). The functions and types are powered by the [supabase-management-js](https://www.npmjs.com/package/supabase-management-js) package, which we maintain.

## Wrap supabase-js in tasks

When working with Trigger.dev [Jobs](https://trigger.dev/docs/documentation/concepts/jobs), it's almost always necessary to wrap operations in a [Task](https://trigger.dev/docs/documentation/concepts/tasks) to enable retries and job [Resumability](https://trigger.dev/docs/documentation/concepts/resumability).

For this, we've also exposed a wrapper for the [supabase-js](https://github.com/supabase/supabase-js) client that allows you to wrap your Supabase operations in a Task:


**NOTE**

_We currently support service_role authentication, which should be kept secret and only used on the server, which is why it's not prefixed with `NEXT_PUBLIC_`._

```ts
import { db } from "./db";
import { Supabase } from "@trigger.dev/supabase";

const supabase = new Supabase({
  id: "supabase",
  supabaseKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
  supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
});

client.defineJob({
  id: "example",
  name: "Example",
  version: "0.1.0",
  trigger: db.onInserted({ table: "todos" }),
  integrations: {
    supabase,
  },
  run: async (payload, io, ctx) => {
    const user = await io.supabase.runTask("fetch-user", async (db) => {
      const { data, error } = await db.auth.admin.getUserById(
        payload.record.user_id
      );

      if (error) {
        throw error;
      }

      return data.user;
    });

    return user;
  },
});
```

## Self-hosting Trigger.dev using Supabase

Trigger.dev is built on top of PostgreSQL. That means you can easily self-host Trigger.dev and run it on your Supabase database (either on Supabase Cloud or self-hosted).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lb7s8o1wsw00gflkmehw.png)

Our contributor [nicktrn](https://github.com/nicktrn) has written up a handy guide on how to self-host Trigger.dev using [Docker and Supabase](https://trigger.dev/docs/documentation/guides/self-hosting/supabase).

## What's next?

We're looking into other ways of integrating Supabase with Trigger.dev, such as hosting and running your jobs on Supabase Edge Functions. If you have any ideas or requests, please let us know!

We're excited to see what you build with Trigger.dev and Supabase. If you have any questions, feedback, or ideas, please join our [Discord](https://discord.gg/nkqV9xBYWy) or head over to our [Github repo](https://github.com/triggerdotdev/trigger.dev) and drop an issue (or a ⭐).