Bringing Supabase support to Trigger.dev
CTO, 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:
_10import functions from "firebase-functions";_10_10export 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:
_10client.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:
_10client.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
:
_18client.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:
_10client.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:
_28client.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:
Then, create a new Next.js app with Supabase support:
_10npx create-next-app@latest -e with-supabase supabase-onboarding-emails_10cd supabase-onboarding-emails
Rename the .env.local.example
file to .env.local
and add your Supabase URL and public key:
_10NEXT_PUBLIC_SUPABASE_URL=your-project-url_10NEXT_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:
_10npx 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)
_10npx @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_10export 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)
_10npm add @trigger.dev/supabase @trigger.dev/resend
Add your Resend.com API key to the .env.local
file:
_10RESEND_API_KEY=your-api-key
Edit the jobs/examples.ts
file and replace with the following code:
_63import { client } from "@/trigger";_63import { Database } from "@/supabase-types";_63import { SupabaseManagement } from "@trigger.dev/supabase";_63import { Resend } from "@trigger.dev/resend";_63_63// Use OAuth to authenticate with Supabase Management API_63const supabaseManagement = new SupabaseManagement({_63 id: "supabase-management",_63});_63_63// Use the types we generated earlier_63const db = supabaseManagement.db<Database>(_63 process.env.NEXT_PUBLIC_SUPABASE_URL!_63);_63_63const resend = new Resend({_63 id: "resend",_63 apiKey: process.env.RESEND_API_KEY!,_63});_63_63client.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 from: "[email protected]",_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 from: "[email protected]",_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:
Authorize access to your Supabase project and then you'll be ready to run the job 🚀
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:
_10npm run dev
And then in a separate terminal, run the following command to start the Trigger.dev agent:
_10npx @trigger.dev/cli dev
Head back to your Supabase Dashboard -> Auth, and create a new user (keep "Auto Confirm User?" checked)
Then navigate over to your Trigger.dev project dashboard and you should see the 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:
_20import { Github } from "@trigger.dev/github";_20_20const github = new Github({_20 id: "github",_20 token: process.env.GITHUB_TOKEN!,_20});_20_20client.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:
_10CREATE OR REPLACE TRIGGER triggername_10AFTER UPDATE on "public"."todos"_10FOR 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:
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 😉)
_27import { client } from "./trigger";_27import { eventTrigger } from "@trigger.dev/sdk";_27import { SupabaseManagement } from "@trigger.dev/supabase";_27_27const supabaseManagement = new SupabaseManagement({_27 id: "supabase-management",_27});_27_27client.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_
.
_33import { db } from "./db";_33import { Supabase } from "@trigger.dev/supabase";_33_33const supabase = new Supabase({_33 id: "supabase",_33 supabaseKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,_33 supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL!,_33});_33_33client.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).
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 ⭐).