-
-
Notifications
You must be signed in to change notification settings - Fork 741
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
Changes from 3 commits
3d50d97
9bc830e
c998c60
20cc1fd
09b0c9f
e1ae353
ff87911
e52a272
f5d3429
0b2b961
619f2d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
|
||
## 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 | ||
} | ||
} | ||
}, | ||
}); | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
|
||
|
||
## 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). |
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
## 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
``` |
There was a problem hiding this comment.
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:
Consistency in imports: In the Remix example,
stripe
is imported in lowercase, while in the Next.js example, it'sStripe
with a capital 'S'. Consider using consistent casing across examples.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
.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:
Apply similar changes to the Next.js example as well.