Skip to content

Supabase Storage and Stripe webhook examples #1345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/examples/ffmpeg-video-processing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ To test this task, use this payload structure:

```json
{
"videoUrl": "<video-url>"
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```

Expand Down Expand Up @@ -250,7 +250,7 @@ To test this task, use this payload structure:

```json
{
"videoUrl": "<video-url>"
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```

Expand Down Expand Up @@ -355,6 +355,6 @@ To test this task in the dashboard, you can use the following payload:

```json
{
"videoUrl": "<video-url>"
"videoUrl": "<video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
2 changes: 2 additions & 0 deletions docs/examples/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ description: "Learn how to use Trigger.dev with these practical task examples."
| [React to PDF](/examples/react-pdf) | Use `react-pdf` to generate a PDF and save it to Cloudflare R2. |
| [Resend email sequence](/examples/resend-email-sequence) | Send a sequence of emails over several days using Resend with Trigger.dev. |
| [Sharp image processing](/examples/sharp-image-processing) | Use Sharp to process an image and save it to Cloudflare R2. |
| [Stripe webhook](/examples/stripe-webhook) | Trigger a task from Stripe webhook events. |
| [Supabase Storage upload](/examples/supabase-storage-upload) | Download a video from a URL and upload it to Supabase Storage using S3. |
| [Vercel AI SDK](/examples/vercel-ai-sdk) | Use Vercel AI SDK to generate text using OpenAI. |
150 changes: 150 additions & 0 deletions docs/examples/stripe-webhook.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
title: "Trigger a task from Stripe webhook events"
sidebarTitle: "Stripe webhook"
description: "This example demonstrates how to handle Stripe webhook events using Trigger.dev."
---

## Overview

This example shows how to set up a webhook handler for incoming Stripe events. The handler triggers a task when a `checkout.session.completed` event is received. This is easily customisable to handle other Stripe events.

## Key features

- Shows how to create a Stripe webhook handler
- Triggers a task when a `checkout.session.completed` event is received

## Environment variables

You'll need to configure the following environment variables for this example to work:

- `STRIPE_WEBHOOK_SECRET` The secret key used to verify the Stripe webhook signature.
- `TRIGGER_API_URL` Your Trigger.dev API url: `https://api.trigger.dev`
- `TRIGGER_API_SECRET` Your Trigger.dev API secret.

## Setting up the Stripe webhook handler

First you'll need to create a [Stripe webhook](https://stripe.com/docs/webhooks) handler route that listens for POST requests and verifies the Stripe signature.

Here are examples of how you can set up a handler using different frameworks:

<CodeGroup>

```ts Next.js
// app/api/stripe-webhook/route.ts
import { NextResponse } from "next/server";
import { tasks } from "@trigger.dev/sdk/v3";
import Stripe from "stripe";
import type { stripeCheckoutCompleted } from "@/trigger/stripe-checkout-completed";
// 👆 **type-only** import

export async function POST(request: Request) {
const signature = request.headers.get("stripe-signature");
const payload = await request.text();

if (!signature || !payload) {
return NextResponse.json(
{ error: "Invalid Stripe payload/signature" },
{
status: 400,
}
);
}

const event = Stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET as string
);

// Perform the check based on the event type
switch (event.type) {
case "checkout.session.completed": {
// Trigger the task only if the event type is "checkout.session.completed"
const { id } = await tasks.trigger<typeof stripeCheckoutCompleted>(
"stripe-checkout-completed",
event
);
return NextResponse.json({ runId: id });
}
default: {
// Return a response indicating that the event is not handled
return NextResponse.json(
{ message: "Event not handled" },
{
status: 200,
}
);
}
}
}
```

```ts Remix
// app/webhooks/stripe.ts
import { type ActionFunctionArgs, json } from "@remix-run/node";
import type { stripeCheckoutCompleted } from "src/trigger/stripe-webhook";
import { tasks } from "@trigger.dev/sdk/v3";
import Stripe from "stripe";

export async function action({ request }: ActionFunctionArgs) {
// Validate the Stripe webhook payload
const signature = request.headers.get("stripe-signature");
const payload = await request.text();

if (!signature || !payload) {
return json({ error: "Invalid Stripe payload/signature" }, { status: 400 });
}

const event = Stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET as string
);

// Perform the check based on the event type
switch (event.type) {
case "checkout.session.completed": {
// Trigger the task only if the event type is "checkout.session.completed"
const { id } = await tasks.trigger<typeof stripeCheckoutCompleted>(
"stripe-checkout-completed",
event
);
return json({ runId: id });
}
default: {
// Return a response indicating that the event is not handled
return json({ message: "Event not handled" }, { status: 200 });
}
}
}
```

</CodeGroup>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve consistency and error handling in code examples

The code examples for Next.js and Remix are well-structured, but there are some improvements that can be made:

  1. Consistency in imports: In the Next.js example, Stripe is imported with a capital 'S', while in the Remix example, it's stripe in lowercase. Consider using consistent casing across examples.

  2. More specific error handling: The current error handling is good, but it could be more specific. Consider adding specific catch blocks for common errors, such as Stripe.errors.StripeSignatureVerificationError.

Here's an example of how you might improve the error handling in both examples:

 const event = Stripe.webhooks.constructEvent(
   payload,
   signature,
   process.env.STRIPE_WEBHOOK_SECRET as string
 );
+} catch (e) {
+  console.error("Error processing Stripe webhook:", e);
+  if (e instanceof Stripe.errors.StripeSignatureVerificationError) {
+    return json({ error: "Invalid signature" }, { status: 400 });
+  } else if (e instanceof Error) {
+    return json({ error: e.message }, { status: 400 });
+  } else {
+    return json({ error: "An unknown error occurred" }, { status: 500 });
+  }
+}

Apply similar changes to both the Next.js and Remix examples.

Committable suggestion was skipped due to low confidence.

Tools
LanguageTool

[uncategorized] ~25-~25: Possible missing comma found.
Context: ... Setting up the Stripe webhook handler First you'll need to create a [Stripe webhook...

(AI_HYDRA_LEO_MISSING_COMMA)


## Task code

This task is triggered when a `checkout.session.completed` event is received from Stripe.

```ts trigger/stripe-webhook.ts
import { task } from "@trigger.dev/sdk/v3";
import type stripe from "stripe";

export const stripeCheckoutCompleted = task({
id: "stripe-checkout-completed",
run: async (payload: stripe.Event, {}) => {
// Add your custom logic for handling the checkout.session.completed event here
},
});
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider enhancing the task code example

The task code provides a good basic structure. To make it more informative, consider adding a more detailed example of what could be done in the run function. This could help users understand how to handle the Stripe event data.

Here's an example of how you might enhance the task code:

 export const stripeCheckoutCompleted = task({
   id: "stripe-checkout-completed",
   run: async (payload: stripe.Event, {}) => {
-    // Add your custom logic for handling the checkout.session.completed event here
+    const session = payload.data.object as stripe.Checkout.Session;
+    console.log(`Processing completed checkout for session ${session.id}`);
+    
+    // Example: Update order status in your database
+    // await updateOrderStatus(session.id, "paid");
+    
+    // Example: Send a confirmation email to the customer
+    // await sendConfirmationEmail(session.customer_email);
   },
 });
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Task code
This task is triggered when a `checkout.session.completed` event is received from Stripe.
```ts trigger/stripe-webhook.ts
import { task } from "@trigger.dev/sdk/v3";
import type stripe from "stripe";
export const stripeCheckoutCompleted = task({
id: "stripe-checkout-completed",
run: async (payload: stripe.Event, {}) => {
// Add your custom logic for handling the checkout.session.completed event here
},
});
```
## Task code
This task is triggered when a `checkout.session.completed` event is received from Stripe.
```ts trigger/stripe-webhook.ts
import { task } from "@trigger.dev/sdk/v3";
import type stripe from "stripe";
export const stripeCheckoutCompleted = task({
id: "stripe-checkout-completed",
run: async (payload: stripe.Event, {}) => {
const session = payload.data.object as stripe.Checkout.Session;
console.log(`Processing completed checkout for session ${session.id}`);
// Example: Update order status in your database
// await updateOrderStatus(session.id, "paid");
// Example: Send a confirmation email to the customer
// await sendConfirmationEmail(session.customer_email);
},
});
```


## Testing your task locally

To test everything is working you can use the Stripe CLI to send test events to your endpoint:

1. Install the [Stripe CLI](https://stripe.com/docs/stripe-cli#install), and login
2. Follow the instructions to [test your handler](https://docs.stripe.com/webhooks#test-webhook). This will include a temporary `STRIPE_WEBHOOK_SECRET` that you can use for testing.
3. When triggering the event, use the `checkout.session.completed` event type. With the Stripe CLI: `stripe trigger checkout.session.completed`
4. If your endpoint is set up correctly, you should see the Stripe events logged in your console with a status of `200`.
5. Then, check the [Trigger.dev](https://cloud.trigger.dev) dashboard and you should see the successful run of the `stripe-webhook` task.

For more information on setting up and testing Stripe webhooks, refer to the [Stripe Webhook Documentation](https://stripe.com/docs/webhooks).
74 changes: 74 additions & 0 deletions docs/examples/supabase-storage-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: "Upload a video to Supabase Storage using S3"
sidebarTitle: "Supabase Storage upload"
description: "This example demonstrates how to download a video from a URL and upload it to Supabase Storage using Trigger.dev."
---

## Overview

This task downloads a video from a provided URL, saves it to a temporary file, and then uploads the video file to Supabase Storage using S3.

## Key features

- Fetches a video from a provided URL
- Uploads the video file to Supabase Storage

## Task code

```ts trigger/supabase-storage-upload.ts
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import fetch from "node-fetch";

// Initialize S3 client for Supabase Storage
const s3Client = new S3Client({
region: process.env.SUPABASE_REGION, // Your Supabase project's region e.g. "us-east-1"
endpoint: `https://${process.env.SUPABASE_PROJECT_ID}.supabase.co/storage/v1/s3`,
credentials: {
// These credentials can be found in your supabase storage settings, under 'S3 access keys'
accessKeyId: process.env.SUPABASE_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.SUPABASE_SECRET_ACCESS_KEY ?? "",
},
});

export const supabaseStorageUpload = task({
id: "supabase-storage-upload",
run: async (payload: { videoUrl: string }) => {
const { videoUrl } = payload;

// Fetch the video as an ArrayBuffer
const response = await fetch(videoUrl);
const videoArrayBuffer = await response.arrayBuffer();
const videoBuffer = Buffer.from(videoArrayBuffer);

const bucket = "my_bucket"; // Replace "my_bucket" with your bucket name
const objectKey = `video_${Date.now()}.mp4`;

// Upload the video directly to Supabase Storage
await s3Client.send(
new PutObjectCommand({
Bucket: bucket,
Key: objectKey,
Body: videoBuffer,
})
);
logger.log(`Video uploaded to Supabase Storage bucket`, { objectKey });

// Return the video object key
return {
objectKey,
bucket: bucket,
};
},
});
```

## Testing your task

To test this task in the dashboard, you can use the following payload:

```json
{
"videoUrl": "<a-video-url>" // Replace <a-video-url> with the URL of the video you want to upload
}
```
2 changes: 2 additions & 0 deletions docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@
"examples/open-ai-with-retrying",
"examples/pdf-to-image",
"examples/sharp-image-processing",
"examples/stripe-webhook",
"examples/supabase-storage-upload",
"examples/react-pdf",
"examples/resend-email-sequence",
"examples/vercel-ai-sdk"
Expand Down