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 3 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
}
```
144 changes: 144 additions & 0 deletions docs/examples/stripe-webhook.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
title: "Trigger a task from a Stripe webhook events"
sidebarTitle: "Stripe webhook"
description: "This example demonstrates how to handle Stripe webhook events using Trigger.dev."
---

## Overview

We will set up a webhook handler for incoming Stripe events. When a Stripe event is received, the handler will trigger a task that can perform an action based on the event type.

## Key features

- Handles Stripe webhook events
- Performs actions based on the event type (e.g., "checkout.session.completed")

## Environment variables

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

- `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 a Stripe webhook handler

To set up your endpoint, you'll need to create a webhook 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 Remix
// app/webhooks/stripe.ts
import { type ActionFunctionArgs, json } from "@remix-run/node";
import type { stripeWebhook } from "src/trigger/stripe-webhook";
import { tasks } from "@trigger.dev/sdk/v3";
import stripe from "stripe";

export async function action({ request, params }: ActionFunctionArgs) {
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}

const signature = request.headers.get("stripe-signature");
const payload = await request.text();

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

try {
// Construct the Stripe event using the payload, signature, and webhook secret
const event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET as string
);

console.log("Received Stripe event:", event);

// Trigger the 'stripe-webhook' task with the constructed event
const { id } = await tasks.trigger<typeof stripeWebhook>("stripe-webhook", event);

// Return a JSON response with the run ID of the triggered task
return json({ runId: id });
} catch (e) {
console.error("Error processing Stripe webhook:", e);
return json({ error: e instanceof Error ? e.message : JSON.stringify(e) }, { status: 400 });
}
}
```

```ts Nextjs
// app/api/stripe-webhook/route.ts
import { NextResponse } from "next/server";
import { tasks } from "@trigger.dev/sdk/v3";
import Stripe from "stripe";
import { stripeWebhook } from "../../../trigger/stripe-webhook";

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,
}
);
}

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

const { id } = await tasks.trigger<typeof stripeWebhook>("stripe-webhook", event);

return NextResponse.json({ runId: id });
} catch (e) {
console.error("Error processing Stripe webhook:", e);
return NextResponse.json(
{ error: e instanceof Error ? e.message : JSON.stringify(e) },
{ status: 400 }
);
}
}
```

</CodeGroup>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestions for improving code examples

The code examples are well-structured, but consider the following improvements:

  1. Consistency in imports: In the Remix example, stripe is imported in lowercase, while in the Next.js example, it's Stripe with a capital 'S'. 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.

  3. Import path in Next.js example: The relative import path "../../../trigger/stripe-webhook" might be confusing. Consider using an absolute import path or explaining the project structure.

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

 } catch (e) {
   console.error("Error processing Stripe webhook:", e);
-  return json({ error: e instanceof Error ? e.message : JSON.stringify(e) }, { status: 400 });
+  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 the Next.js example as well.

Committable suggestion was skipped due to low confidence.


## Task code

This task listens for Stripe events and performs actions based on the `checkout.session.completed` event type:

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

export const stripeWebhook = task({
id: "stripe-webhook",
run: async (payload: stripe.Event, { ctx }) => {
switch (payload.type) {
case "checkout.session.completed": {
//do stuff
}
}
},
});
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Enhance task code with more detailed examples

The task code provides a good basic structure for handling Stripe events. However, it could be improved by adding more detailed comments or examples of what actions could be performed for different event types. Consider expanding the checkout.session.completed case with a more concrete example:

 export const stripeWebhook = task({
   id: "stripe-webhook",
   run: async (payload: stripe.Event, { ctx }) => {
     switch (payload.type) {
       case "checkout.session.completed": {
-        //do stuff
+        // Example: Update order status in database
+        const session = payload.data.object as stripe.Checkout.Session;
+        await updateOrderStatus(session.id, "paid");
+        
+        // Send confirmation email to customer
+        await sendConfirmationEmail(session.customer_email);
       }
+      // Add more cases for other event types
+      // case "payment_intent.succeeded":
+      // case "customer.subscription.created":
     }
   },
 });

This enhancement would provide users with a clearer understanding of how to implement specific actions based on different Stripe event types.

Committable suggestion was skipped due to low confidence.


## 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 to test your endpoint.
3. If your endpoint is set up correctly, you should see the Stripe events logged in your console with a status of 200.
4. 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).
86 changes: 86 additions & 0 deletions docs/examples/supabase-storage-upload.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
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 fs from "fs/promises";
import fetch from "node-fetch";
import os from "os";
import path from "path";

// Initialize S3 client for Supabase Storage
// Update your .env file, and environment variables in the dashboard as instructed below
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`, // Your Supabase project ID
credentials: {
// These credentials can be found in your supabase storage settings, under 'S3 access keys'
accessKeyId: process.env.SUPABASE_S3_ACCESS_KEY_ID ?? "", // Access key ID
secretAccessKey: process.env.SUPABASE_S3__SECRET_ACCESS_KEY ?? "", // Access key
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Fix typo and improve error handling in S3 client initialization

Good use of environment variables for security. However, there are a few improvements to be made:

  1. There's a typo in the environment variable name on line 34. It should be SUPABASE_S3_SECRET_ACCESS_KEY instead of SUPABASE_S3__SECRET_ACCESS_KEY.

  2. Consider throwing an error if the environment variables are not set, instead of using empty strings as fallbacks. This will help catch configuration issues early.

  3. The comment on line 29 hardcodes the region to "us-east-1", which might be misleading. Consider removing the specific region from the comment.

Here's a suggested improvement:

 const s3Client = new S3Client({
-  region: process.env.SUPABASE_REGION, // Your Supabase project's region e.g. "us-east-1"
+  region: process.env.SUPABASE_REGION, // Your Supabase project's region
   endpoint: `https://${process.env.SUPABASE_PROJECT_ID}.supabase.co/storage/v1/s3`, // Your Supabase project ID
   credentials: {
     // These credentials can be found in your supabase storage settings, under 'S3 access keys'
-    accessKeyId: process.env.SUPABASE_S3_ACCESS_KEY_ID ?? "", // Access key ID
-    secretAccessKey: process.env.SUPABASE_S3__SECRET_ACCESS_KEY ?? "", // Access key
+    accessKeyId: process.env.SUPABASE_S3_ACCESS_KEY_ID, // Access key ID
+    secretAccessKey: process.env.SUPABASE_S3_SECRET_ACCESS_KEY, // Secret access key
   },
 });

+if (!process.env.SUPABASE_REGION || !process.env.SUPABASE_PROJECT_ID || !process.env.SUPABASE_S3_ACCESS_KEY_ID || !process.env.SUPABASE_S3_SECRET_ACCESS_KEY) {
+  throw new Error("Missing required Supabase environment variables");
+}

This change will ensure that all necessary environment variables are present and will throw an error if any are missing.

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
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import fs from "fs/promises";
import fetch from "node-fetch";
import os from "os";
import path from "path";
// Initialize S3 client for Supabase Storage
// Update your .env file, and environment variables in the dashboard as instructed below
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`, // Your Supabase project ID
credentials: {
// These credentials can be found in your supabase storage settings, under 'S3 access keys'
accessKeyId: process.env.SUPABASE_S3_ACCESS_KEY_ID ?? "", // Access key ID
secretAccessKey: process.env.SUPABASE_S3__SECRET_ACCESS_KEY ?? "", // Access key
},
});
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import fs from "fs/promises";
import fetch from "node-fetch";
import os from "os";
import path from "path";
// Initialize S3 client for Supabase Storage
// Update your .env file, and environment variables in the dashboard as instructed below
const s3Client = new S3Client({
region: process.env.SUPABASE_REGION, // Your Supabase project's region
endpoint: `https://${process.env.SUPABASE_PROJECT_ID}.supabase.co/storage/v1/s3`, // Your Supabase project ID
credentials: {
// These credentials can be found in your supabase storage settings, under 'S3 access keys'
accessKeyId: process.env.SUPABASE_S3_ACCESS_KEY_ID, // Access key ID
secretAccessKey: process.env.SUPABASE_S3_SECRET_ACCESS_KEY, // Secret access key
},
});
if (!process.env.SUPABASE_REGION || !process.env.SUPABASE_PROJECT_ID || !process.env.SUPABASE_S3_ACCESS_KEY_ID || !process.env.SUPABASE_S3_SECRET_ACCESS_KEY) {
throw new Error("Missing required Supabase environment variables");
}


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

// Generate temporary file path
const tempDirectory = os.tmpdir();
const outputPath = path.join(tempDirectory, `video_${Date.now()}.mp4`);

// Fetch the video and save it to a temporary file
const response = await fetch(videoUrl);
const videoBuffer = await response.arrayBuffer();
await fs.writeFile(outputPath, Buffer.from(videoBuffer));

const videoFile = await fs.readFile(outputPath); // Read the video file
const bucket = "my_bucket"; // Replace "my_bucket" with your bucket name

// Upload the video to Supabase Storage
const objectKey = path.basename(outputPath);
await s3Client.send(
new PutObjectCommand({
Bucket: bucket,
Key: objectKey,
Body: videoFile,
})
);
logger.log(`Video uploaded to Supabase Storage bucket`, { objectKey });

// Delete the temporary video file
await fs.unlink(outputPath);

// Return the video object key
return {
objectKey,
bucket: bucket,
};
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Improve error handling and flexibility in the upload task

The task implementation is generally correct, but there are several areas for improvement:

  1. Error handling: Consider wrapping the main logic in a try-catch block to handle potential errors gracefully.

  2. Bucket name: The bucket name is hardcoded, which might not be flexible for all users. Consider making it a parameter or an environment variable.

  3. File extension: The temporary file always uses .mp4 extension, which might not be correct for all video types. Consider extracting the extension from the original URL or using a more generic name.

  4. Filename generation: Using Date.now() for generating unique filenames might lead to collisions in high-concurrency scenarios. Consider using a more robust method like UUID.

Here's a suggested refactoring that addresses these issues:

import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { logger, task } from "@trigger.dev/sdk/v3";
import fs from "fs/promises";
import fetch from "node-fetch";
import os from "os";
import path from "path";
import { v4 as uuidv4 } from 'uuid';

// ... S3 client initialization ...

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

    if (!bucket) {
      throw new Error("Bucket name is required. Provide it in the payload or set SUPABASE_DEFAULT_BUCKET environment variable.");
    }

    try {
      // Generate temporary file path with UUID
      const tempDirectory = os.tmpdir();
      const fileExtension = path.extname(new URL(videoUrl).pathname) || '.video';
      const outputPath = path.join(tempDirectory, `${uuidv4()}${fileExtension}`);

      // Fetch the video and save it to a temporary file
      const response = await fetch(videoUrl);
      if (!response.ok) {
        throw new Error(`Failed to fetch video: ${response.statusText}`);
      }
      const videoBuffer = await response.arrayBuffer();
      await fs.writeFile(outputPath, Buffer.from(videoBuffer));

      const videoFile = await fs.readFile(outputPath);

      // Upload the video to Supabase Storage
      const objectKey = path.basename(outputPath);
      await s3Client.send(
        new PutObjectCommand({
          Bucket: bucket,
          Key: objectKey,
          Body: videoFile,
        })
      );
      logger.log(`Video uploaded to Supabase Storage bucket`, { objectKey, bucket });

      // Delete the temporary video file
      await fs.unlink(outputPath);

      // Return the video object key and bucket
      return { objectKey, bucket };
    } catch (error) {
      logger.error("Error in supabaseStorageUpload task", { error });
      throw error;
    }
  },
});

This refactored version includes better error handling, more flexible bucket naming, improved file extension handling, and uses UUID for unique filename generation. Make sure to install and import the uuid package if you use this suggestion.

```

## 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
Loading