Skip to content

Fix: implement server-side Razorpay webhook verification for robust payment sync#11

Open
anveshpol1522 wants to merge 7 commits into
notsoocool:mainfrom
anveshpol1522:main
Open

Fix: implement server-side Razorpay webhook verification for robust payment sync#11
anveshpol1522 wants to merge 7 commits into
notsoocool:mainfrom
anveshpol1522:main

Conversation

@anveshpol1522
Copy link
Copy Markdown

Changes made:

1.Updated app/api/razorpay/create-order/route.ts to include the userId in the notes payload sent to Razorpay.

2.Created a new webhook endpoint at app/api/webhooks/razorpay/route.ts to receive and verify the payment.captured event.

3.Implemented cryptographic signature verification to ensure the webhook payload originates legitimately from Razorpay.

4.Used the Supabase Service Role key in the webhook endpoint to securely bypass Row Level Security (RLS) and update the user_usage and user_subscriptions tables.

ACTION REQUIRED BY MAINTAINERS BEFORE MERGE/DEPLOY:

Because this feature relies on secure server-to-server communication, it requires the addition of two new environment variables and a dashboard configuration.

  1. Add New Environment Variables
    Please add the following keys to your .env.local (for testing) and your Vercel/hosting environment variables (for production):

    RAZORPAY_WEBHOOK_SECRET: A secret string used to verify the webhook signature. (You will create this in step 2).

    SUPABASE_SERVICE_ROLE_KEY: Your Supabase Service Role key (found in Supabase Dashboard -> Project Settings -> API). This is required because the webhook operates outside of the Clerk authenticated session.

  2. Configure the Webhook in Razorpay
    In your Razorpay Dashboard:

    Navigate to Settings ---> Webhooks.

    Click Add New Webhook.

    Webhook URL: Set this to your production domain (e.g., https://worktowords.vercel.app/api/webhooks/razorpay). For local testing, you will need a tool like Ngrok or Stripe CLI to forward the local port.

    Secret: Create a strong, random secret string. This must match the RAZORPAY_WEBHOOK_SECRET environment variable.

    Active Events: Select ONLY payment.captured.

    Save the webhook.

Testing Notes for Maintainers:
To test this locally, you will need to expose your localhost:3000 to the internet using a tool like Ngrok or Localtunnel so Razorpay can reach the webhook endpoint during a test transaction.

A huge thanks for assigning this issue to me. Absolutely loved working on this issue and would be glad to work on future issues like this

@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

@anveshpol1522 is attempting to deploy a commit to the notsoocool's projects Team on Vercel.

A member of the Team first needs to authorize it.

@notsoocool
Copy link
Copy Markdown
Owner

Hey @anveshpol1522 — thanks for picking up #8, this is exactly the kind of gap we needed to close.

I went through the PR and I'm not quite ready to merge yet. The webhook direction is right, but a couple things would break production if we shipped as-is.

The big one: replacing create-order removes stuff the app already relies on (keyId, prefill, rate limits, saving the order in Supabase). Checkout would stop working because the frontend needs keyId to open Razorpay. Could you keep the existing postCreateOrder flow and only add userId to the order notes in paymentService?

Also Razorpay won't be able to hit the webhook unless we mark /api/webhooks/razorpay as a public route in proxy.ts — right now Clerk would block it.

Smaller stuff: the upsert uses onConflict razorpay_order_id but our PK is user_id, so we should match what verifyPayment already does. Would be good to skip upgrading if that payment was already processed (client verify might've beaten the webhook). And plan_expiry should stay YYYY-MM-DD like the rest of the code.

Ideally most of the upgrade logic lives in paymentService so we're not maintaining two versions. A README note for RAZORPAY_WEBHOOK_SECRET would help too.

Ping me when you push an update — happy to take another look.

@anveshpol1522
Copy link
Copy Markdown
Author

Hey @notsoocool, thanks for the guidance! I've updated the PR based on your feedback.

Here is what I changed:

Reverted create-order: Restored app/api/razorpay/create-order/route.ts to its original state so the frontend checkout flow (keyId, prefill, etc.) remains intact.

Centralized Logic: Moved the upgrade logic into a new processProUpgrade function inside lib/paymentService.ts. I also updated the existing client-side verifyPayment function to use this shared logic so we aren't maintaining two versions.

Idempotency & Primary Key: The shared logic now uses user_id as the Primary Key for the upsert and includes an idempotency check to skip the webhook upgrade if the client has already processed that specific payment_id.

Date Formatting: Ensured the plan_expiry remains strictly YYYY-MM-DD.

Order Notes: Added the userId to the notes object in the postCreateOrder flow inside paymentService.

Public Route: Whitelisted /api/webhooks/razorpay in proxy.ts so Razorpay can successfully hit the endpoint without Clerk blocking it.

Let me know if this looks good to merge!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Potential Payment Desync: Missing Razorpay Webhook Integration for Server-Side Verification

2 participants