Repository navigation
feat: add Payments and Subscriptions with Dodo Payments templates - #366
adityaoberai wants to merge 4 commits into
Conversation
One-time checkout through a Dodo Payments checkout session. The payment.succeeded webhook is verified with Standard Webhooks and stores the order as a row in an Appwrite table, using the payment ID as the row ID so repeated deliveries don't store it twice. The database and table are created on the first order, with the columns inline in createTable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaBGMrHMJFCb9gAU8rQb6A
Subscription checkout through a Dodo Payments checkout session. Every subscription.* webhook sets the subscriber label from the subscription's current status rather than the event type: active and past_due keep access, every other status removes it. A late or out-of-order event therefore can't give access back after a cancellation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaBGMrHMJFCb9gAU8rQb6A
🔵 Tier A · Mergeable after minor fixes
Adds two Node.js Dodo Payments function templates: one creates one-time checkout sessions and stores successful orders in Appwrite TablesDB, while the other creates subscription checkouts and manages a subscriber label from subscription status. Both templates include signed webhook handling, demo pages, dependency lockfiles, and setup documentation. Latest changes: The latest commits add a
Note Not approving while a bug finding is open: Default verification URL can point at the frontend
Fix with agent prompt### Issue 1
node/payments-with-dodo-payments/src/main.js:97-98
**Default verification URL can point at the frontend**
When callers use a separate frontend `successUrl` (as in the README's `https://example.com/success` example), this resolves `/success` on that frontend origin rather than on the function, so Dodo bypasses the new redirect-based verification and fulfillment. The subscription template has the same default; derive it from the function request origin and reserve `successUrl` for the final redirect.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.📂 Walkthrough · 8
✅ Fixed since the last review · 1
Reviewed the commits since |
|
Functions are tested and confirmed to be working. Paymentshttps://6ac7fea7c50f765c7b50.fra.appwrite.run/
Subscriptionshttps://6ac7fea7ced08d4a4341.fra.appwrite.run/
Appwrite projecthttps://appwrite.io/projects/dodo-payments-test/auth/users/6ac8039c06d26e79ca55 |
Both demo pages now use the appwrite.io design tokens: the dark background, card and border colours, the pink call-to-action button and the gradient hero title. Light mode follows the visitor's system setting. Inter loads from jsDelivr on a caret range, and the Pink stylesheets are gone, with inline SVG for the three icons. Also adds a refresh button for the subscription status and disables the checkout buttons while a checkout is being created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaBGMrHMJFCb9gAU8rQb6A
| ); | ||
| return res.redirect(checkout.checkout_url, 303); | ||
|
|
||
| case '/webhook': |
There was a problem hiding this comment.
We should almost never trust the webhook payload, even if coming from the source, we must call the dodo sdk to check the current payment/subscription status, this applies to both functions webhooks
There was a problem hiding this comment.
Why is that so, considering these are signed webhooks?
The webhook now fetches the payment or subscription from Dodo Payments instead of trusting the event payload. Checkout returns to a new GET /success route that runs the same check and provisioning before redirecting to successUrl, so either path can provision the user. Order row IDs and idempotent label updates make concurrent runs safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| const verifyUrl = | ||
| body.verifyUrl ?? new URL('/success', successUrl).toString(); |
There was a problem hiding this comment.
Default verification URL can point at the frontend
When callers use a separate frontend successUrl (as in the README's https://example.com/success example), this resolves /success on that frontend origin rather than on the function, so Dodo bypasses the new redirect-based verification and fulfillment. The subscription template has the same default; derive it from the function request origin and reserve successUrl for the final redirect.
Prompt To Fix With AI
This is a comment left during a code review.
Path: node/payments-with-dodo-payments/src/main.js
Line: 97-98
Comment:
**Default verification URL can point at the frontend**
When callers use a separate frontend `successUrl` (as in the README's `https://example.com/success` example), this resolves `/success` on that frontend origin rather than on the function, so Dodo bypasses the new redirect-based verification and fulfillment. The subscription template has the same default; derive it from the function request origin and reserve `successUrl` for the final redirect.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.🟡 Minor · bug · Reply if this doesn't apply.




What
Adds two Node.js function templates for Dodo Payments. They follow the Lemon Squeezy pair closely, so they diff cleanly against them.
node/payments-with-dodo-paymentsPOST /checkoutfor a one-time productpayment.succeededstores the order as a row in an Appwrite tablenode/subscriptions-with-dodo-paymentsPOST /subscribefor a subscription productsubscription.*event adds or removes asubscriberlabel on the userBoth serve a demo page at
GET /. The webhook checks the Standard Webhooks signature through the Dodo SDK'swebhooks.unwrapand answers401when it doesn't match.Dependencies are
dodopayments^2.54.0,node-appwrite^29.1.0 andprettier^3.9.9. The demo pages loadappwrite@^28.1.0andalpinejs@^3.17.4from jsDelivr.Decisions worth reviewing
activeandpast_duekeep the label, sincepast_dueis Dodo's grace period while it retries a renewal. Every other status removes it. A delayedsubscription.activethat lands after a cancellation carriescancelled, so it can't hand access back. A switch on event type would.409fromcreateRowmeans the order is already stored.getTableand, on404, creates the database and the table. The columns go inline increateTable, and Appwrite makes them available in the same request, so the row write right after can't race attribute creation.DODO_PAYMENTS_ENVIRONMENTfalls back totest_mode, including when it's set to an empty string.200. Dodo retries any non-2xx response for about 28 hours, so an event withoutmetadata.user_idis logged and acknowledged.customeronly when the page provides an email. Test mode sends real emails, so there's no placeholder address.Fixed from the Lemon Squeezy templates
subscriberagain when the user already has it.npm run setupbuild step, which doesn't exist.req.bodyJsonandreq.bodyTextinstead of the deprecatedreq.body.Testing
npm ci --ignore-scripts --no-audit --no-fund,node --checkon everysrc/*.jsandnpm auditpass in both templates. The audit reports 0 vulnerabilities.standardwebhooksand a throwaway secret, and the Appwrite and Dodo HTTP calls were stubbed. The cases cover these paths.401.payment.succeededcreates the database, the table and the row. A second delivery stores nothing.active,past_due,cancelledand every other status, plus a latesubscription.activethat carriescancelled.Before this leaves draft
data.metadata.user_idon both payments and subscriptions.users.getreturns404and the subscriptions webhook fails, so Dodo retries it for about 28 hours. It could be acknowledged with a200like the missinguser_idcase.The workflow on main regenerates the root README table, so this PR doesn't touch it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01TaBGMrHMJFCb9gAU8rQb6A