Bringing Supabase support to Trigger.dev

Eric AllamEric Allam

Bringing Supabase support to Trigger.dev

A big part of our inspiration when starting Trigger.dev was the experience of using Firestore Cloud Functions to build some complex backend orchestration tasks built on top of triggering functions whenever a document was created, updated, or deleted:


_10
import functions from "firebase-functions";
_10
_10
export const myFunction = functions.firestore
_10
.document("todos/{todoId}")
_10
.onWrite((change, context) => {
_10
/* ... */
_10
});

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, self-hostable project.

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


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

Introducing @trigger.dev/supabase

Well, we're happy to announce that it is with our new Supabase integration. 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:


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

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


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

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


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

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


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

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, 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) and choose the "User Management Starter" Quickstart:

management starter

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


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

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


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

Finally, we'll go ahead and Generate the Typescript types now as we'll be using them in a bit:


_10
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 for an account first and create your first project)


_10
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:


_10
// Match all routes other than /api/trigger
_10
export const config = {
_10
matcher: ["/((?!api/trigger).*)"],
_10
};

Add Supabase & Resend integrations

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


_10
npm add @trigger.dev/supabase @trigger.dev/resend

Add your Resend.com API key to the .env.local file:


_10
RESEND_API_KEY=your-api-key

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


_63
import { client } from "@/trigger";
_63
import { Database } from "@/supabase-types";
_63
import { SupabaseManagement } from "@trigger.dev/supabase";
_63
import { Resend } from "@trigger.dev/resend";
_63
_63
// Use OAuth to authenticate with Supabase Management API
_63
const supabaseManagement = new SupabaseManagement({
_63
id: "supabase-management",
_63
});
_63
_63
// Use the types we generated earlier
_63
const db = supabaseManagement.db<Database>(
_63
process.env.NEXT_PUBLIC_SUPABASE_URL!
_63
);
_63
_63
const resend = new Resend({
_63
id: "resend",
_63
apiKey: process.env.RESEND_API_KEY!,
_63
});
_63
_63
client.defineJob({
_63
id: "welcome-email-campaign",
_63
name: "Welcome Email Campaign",
_63
version: "1.0.0",
_63
trigger: db.onUpdated({
_63
// Trigger this job whenever a user is confirmed
_63
schema: "auth",
_63
table: "users",
_63
filter: {
_63
old_record: {
_63
email_confirmed_at: [{ $isNull: true }],
_63
},
_63
record: {
_63
email_confirmed_at: [{ $isNull: false }],
_63
},
_63
},
_63
}),
_63
integrations: {
_63
resend,
_63
},
_63
run: async (payload, io, ctx) => {
_63
// payload.record is typed based on the Supabase schema
_63
if (!payload.record.email) {
_63
return;
_63
}
_63
_63
await io.resend.sendEmail("email-1", {
_63
to: payload.record.email,
_63
subject: `Thanks for joining Acme Inc`,
_63
text: `Hi there, welcome to our community! This is the first email we send you to help you get started.`,
_63
_63
});
_63
_63
await io.wait("⌛", 60); // Waits for 1 minute but you might want to wait longer
_63
_63
await io.resend.sendEmail("email-2", {
_63
to: payload.record.email,
_63
subject: `Here are some tips to get started`,
_63
text: `Hi there, welcome to our community! This is the second email we send you to help you get started.`,
_63
_63
});
_63
},
_63
});

Authenticate to the Supabase Management API

The Supabase Triggers use the Supabase Management API 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:

configure

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

oauth

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:


_10
npm run dev

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


_10
npx @trigger.dev/cli dev

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

create user

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

job running

How it works

Trigger.dev supports triggering jobs through 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:


_20
import { Github } from "@trigger.dev/github";
_20
_20
const github = new Github({
_20
id: "github",
_20
token: process.env.GITHUB_TOKEN!,
_20
});
_20
_20
client.defineJob({
_20
id: "github-integration-on-issue",
_20
name: "GitHub Integration - On Issue",
_20
version: "0.1.0",
_20
trigger: github.triggers.repo({
_20
event: events.onIssue,
_20
owner: "triggerdotdev",
_20
repo: "trigger.dev",
_20
}),
_20
run: async (payload, io, ctx) => {
_20
//do stuff
_20
},
_20
});

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.

This is exactly how the Supabase Integration works, except it uses Supabase 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:


_10
CREATE OR REPLACE TRIGGER triggername
_10
AFTER UPDATE on "public"."todos"
_10
FOR EACH ROW
_10
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:

How it works Diagram

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 😉)


_27
import { client } from "./trigger";
_27
import { eventTrigger } from "@trigger.dev/sdk";
_27
import { SupabaseManagement } from "@trigger.dev/supabase";
_27
_27
const supabaseManagement = new SupabaseManagement({
_27
id: "supabase-management",
_27
});
_27
_27
client.defineJob({
_27
id: "create-supbase-project",
_27
name: "Create Supabase Project",
_27
version: "0.1.0",
_27
trigger: eventTrigger({ name: "user.created" }),
_27
integrations: {
_27
supabaseManagement,
_27
},
_27
run: async (payload, io) => {
_27
await io.supabaseManagement.createProject("🚀", {
_27
db_pass: payload.password,
_27
name: payload.name,
_27
organization_id: process.env.SUPABASE_ORGANIZATION_ID,
_27
plan: "pro",
_27
region: "us-east-1",
_27
kps_enabled: true,
_27
});
_27
},
_27
});

For more information on what can be done in your Supabase Account, check out the Supabase Management API reference. The functions and types are powered by the supabase-management-js package, which we maintain.

Wrap supabase-js in tasks

When working with Trigger.dev Jobs, it's almost always necessary to wrap operations in a Task to enable retries and job Resumability.

For this, we've also exposed a wrapper for the 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_.


_33
import { db } from "./db";
_33
import { Supabase } from "@trigger.dev/supabase";
_33
_33
const supabase = new Supabase({
_33
id: "supabase",
_33
supabaseKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
_33
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,
_33
});
_33
_33
client.defineJob({
_33
id: "example",
_33
name: "Example",
_33
version: "0.1.0",
_33
trigger: db.onInserted({ table: "todos" }),
_33
integrations: {
_33
supabase,
_33
},
_33
run: async (payload, io, ctx) => {
_33
const user = await io.supabase.runTask("fetch-user", async (db) => {
_33
const { data, error } = await db.auth.admin.getUserById(
_33
payload.record.user_id
_33
);
_33
_33
if (error) {
_33
throw error;
_33
}
_33
_33
return data.user;
_33
});
_33
_33
return user;
_33
},
_33
});

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).

meme

Our contributor nicktrn has written up a handy guide on how to self-host Trigger.dev using Docker and 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 or head over to our Github repo and drop an issue (or a ⭐).