Skip to content

feat: add Payments and Subscriptions with Dodo Payments templates - #366

Open
adityaoberai wants to merge 4 commits into
mainfrom
feat-dodo-payments-templates
Open

adityaoberai wants to merge 4 commits into
mainfrom
feat-dodo-payments-templates

Conversation

@adityaoberai

Copy link
Copy Markdown
Contributor

What

Adds two Node.js function templates for Dodo Payments. They follow the Lemon Squeezy pair closely, so they diff cleanly against them.

Template Checkout Webhook
node/payments-with-dodo-payments POST /checkout for a one-time product payment.succeeded stores the order as a row in an Appwrite table
node/subscriptions-with-dodo-payments POST /subscribe for a subscription product Every subscription.* event adds or removes a subscriber label on the user

Both serve a demo page at GET /. The webhook checks the Standard Webhooks signature through the Dodo SDK's webhooks.unwrap and answers 401 when it doesn't match.

Dependencies are dodopayments ^2.54.0, node-appwrite ^29.1.0 and prettier ^3.9.9. The demo pages load appwrite@^28.1.0 and alpinejs@^3.17.4 from jsDelivr.

Decisions worth reviewing

  • Access follows the subscription status, not the event type. Dodo can deliver events late, twice or out of order, but each delivery carries the subscription's current status. active and past_due keep the label, since past_due is Dodo's grace period while it retries a renewal. Every other status removes it. A delayed subscription.active that lands after a cancellation carries cancelled, so it can't hand access back. A switch on event type would.
  • A duplicate delivery can't store an order twice. The row ID is the Dodo payment ID, which is 25 characters and a valid Appwrite ID. A 409 from createRow means the order is already stored.
  • Setup runs in the webhook, before the first row. The function calls getTable and, on 404, creates the database and the table. The columns go inline in createTable, and Appwrite makes them available in the same request, so the row write right after can't race attribute creation.
  • Test mode is the default. The SDK defaults to live mode, so DODO_PAYMENTS_ENVIRONMENT falls back to test_mode, including when it's set to an empty string.
  • Events that can never succeed get a 200. Dodo retries any non-2xx response for about 28 hours, so an event without metadata.user_id is logged and acknowledged.
  • The customer prefill is optional. The function sends customer only when the page provides an email. Test mode sends real emails, so there's no placeholder address.

Fixed from the Lemon Squeezy templates

  • Setup is awaited before the write. Lemon Squeezy starts it unawaited from the constructor and adds attributes one at a time.
  • Adding and removing the label is idempotent. Lemon Squeezy pushes subscriber again when the user already has it.
  • The orders page shows "No orders yet" before the table exists. Lemon Squeezy's page stays on "Loading orders.." forever.
  • The READMEs list the scopes the function needs and drop the npm run setup build step, which doesn't exist.
  • The pages use TablesDB and the Web SDK 28 object parameters. The function reads req.bodyJson and req.bodyText instead of the deprecated req.body.

Testing

  • npm ci --ignore-scripts --no-audit --no-fund, node --check on every src/*.js and npm audit pass in both templates. The audit reports 0 vulnerabilities.
  • I ran the default export against mocks, 19 cases for payments and 20 for subscriptions. Events were signed with standardwebhooks and a throwaway secret, and the Appwrite and Dodo HTTP calls were stubbed. The cases cover these paths.
    • A missing header, a wrong secret, a tampered body and a 10 minute old timestamp all return 401.
    • The first payment.succeeded creates the database, the table and the row. A second delivery stores nothing.
    • active, past_due, cancelled and every other status, plus a late subscription.active that carries cancelled.
    • The exact checkout request sent to Dodo, and the test and live API hosts.
  • The CDN URLs return 200. The Web SDK range resolves to 28.1.0, not the 28.2.0 release candidate.

Before this leaves draft

  • A live run against Dodo test mode and Appwrite Cloud. It needs the functions deployed first, because the webhook URL is the function domain. The first real webhooks should confirm that the checkout metadata arrives as data.metadata.user_id on both payments and subscriptions.
  • One open question. If the Appwrite user has been deleted, users.get returns 404 and the subscriptions webhook fails, so Dodo retries it for about 28 hours. It could be acknowledged with a 200 like the missing user_id case.

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

adityaoberai and others added 2 commits October 9, 2026 01:55
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
@adityaoberai
adityaoberai marked this pull request as ready for review October 8, 2026 21:03
@hansi-codes

hansi-codes Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🔵 Tier A · Mergeable after minor fixes

The new redirect-based verification misses the common case where the success page is hosted on a separate frontend origin, while the webhook remains a working fulfillment path.

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 /success return handler to both templates, fetch canonical payment or subscription state before provisioning, and route Dodo's checkout return through that handler.

Verdict New comments Fixed Still open
💬 Commented 1 1 0

Note

Not approving while a bug finding is open: Default verification URL can point at the frontend

Finding Where
🟡 Default verification URL can point at the frontend node/payments-with-dodo-payments/src/main.js:97
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
File Change
node/payments-with-dodo-payments/README.md Documents checkout inputs, the payment verification redirect, webhook behavior, setup, permissions, and environment variables.
node/payments-with-dodo-payments/package.json, package-lock.json, .gitignore, .prettierrc.json Adds the payments template dependencies, lockfile, ignore rules, and formatter configuration.
node/payments-with-dodo-payments/src/appwrite.js, src/dodopayments.js, src/main.js, src/utils.js Implements Dodo checkout and webhook handling, Appwrite order storage, shared response helpers, and payment verification on the return route.
node/payments-with-dodo-payments/static/index.html Adds the payments demo page for registration, checkout, and viewing orders.
node/subscriptions-with-dodo-payments/README.md Documents subscription checkout, status-based label updates, the verification redirect, setup, permissions, and environment variables.
node/subscriptions-with-dodo-payments/package.json, package-lock.json, .gitignore, .prettierrc.json Adds the subscriptions template dependencies, lockfile, ignore rules, and formatter configuration.
node/subscriptions-with-dodo-payments/src/appwrite.js, src/dodopayments.js, src/main.js, src/utils.js Implements subscription checkout and webhook handling, Appwrite label updates, shared response helpers, and subscription verification on the return route.
node/subscriptions-with-dodo-payments/static/index.html Adds the subscriptions demo page for registration, subscription checkout, and subscription status.
✅ Fixed since the last review · 1
  • Webhook status may be stale after cancellation · node/subscriptions-with-dodo-payments/src/main.js:89

Reviewed the commits since 69f80e2 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Tier A · See the inline comments. Summary

Comment thread node/subscriptions-with-dodo-payments/src/main.js Outdated
@adityaoberai

Copy link
Copy Markdown
Contributor Author

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':

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

@adityaoberai adityaoberai Oct 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Tier A · See the inline comments. Summary

Comment on lines +97 to +98
const verifyUrl =
body.verifyUrl ?? new URL('/success', successUrl).toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

2 participants