Repository navigation
feat: add Payments and Subscriptions with Dodo Payments templates - #366
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 S · Ready to merge
Adds two Node.js function templates for Dodo Payments: one-time checkout with Appwrite order storage, and subscription checkout with user-label access management. Each template includes signed webhook processing, a demo page, and setup documentation; the latest updates also acknowledge subscription events for users who no longer exist. Latest changes: The newest commit treats Appwrite 404s during subscription label updates as deleted users and documents that those events, as well as events without user metadata, are acknowledged instead of retried.
📂 Walkthrough · 10
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
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…evoking access The checkout defaulted verifyUrl to /success on the successUrl origin. With a frontend on another origin, that sends the buyer to a page that doesn't exist. Called through the Appwrite API, the function can't tell its own domain either, so the caller now passes verifyUrl. Without it, Dodo Payments returns the buyer straight to successUrl and the webhook provisions them. The demo pages pass their own /success URL. The subscriptions /success route could read a pending subscription and remove the label after a webhook had already added it. It now only gives the label and leaves removal to the webhooks. Also runs prettier on both templates and updates the READMEs to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run could look up a subscription, then apply that status after another run had already applied a newer one. For example, /success could read active, a cancellation webhook could remove the label, and /success would then add it back. The same could happen between two webhooks in either direction. After updating the label, the function now looks the subscription up again and applies the newer status if it changed in the meantime. The READMEs also say where successUrl and failureUrl default to, and that the redirect URLs come from the caller and should be checked against your own domains before going live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the Appwrite user from a subscription's metadata has been deleted, the label update returns a 404 and the webhook failed, so Dodo Payments kept retrying it for about 28 hours. The function now logs it and responds with 200, like it already does for a subscription without a user_id. Other Appwrite errors still fail the webhook so it's retried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@atharvadeosthale ready for another look. Since your review:
Both templates are tested end to end against Dodo test mode with the real checkout. The details are in the description. |
Registers three Node function templates from appwrite/templates: - node-typescript/postgres-with-drizzle (appwrite/templates#362), a todo API on Appwrite Postgres through Drizzle ORM. It builds with tsc and runs dist/main.js. - node/payments-with-dodo-payments and node/subscriptions-with-dodo-payments (appwrite/templates#366), which follow the Lemon Squeezy pair. The entries point at templates 1.4.0, the first tag that will contain them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>




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 productnode/subscriptions-with-dodo-paymentsPOST /subscribefor a subscription productsubscriberlabel on the userBoth serve a demo page at
GET /, styled after appwrite.io. Dependencies aredodopayments^2.54.0,node-appwrite^29.1.0 andprettier^3.9.9. The demo pages loadappwrite@^28.1.0,alpinejs@^3.17.4and Inter from jsDelivr.How a purchase is verified
Two paths provision the user, and either one is enough.
POST /webhookchecks the Standard Webhooks signature and answers401when it doesn't match. It then looks the payment or subscription up in Dodo Payments instead of trusting the event payload.GET /successis where Dodo Payments returns the buyer when the checkout request setsverifyUrl. It looks the payment or subscription up the same way, provisions the user, and redirects tosuccessUrl.Running both is safe. Orders use the Dodo payment ID as the row ID, so a second write gets a
409, and label updates are idempotent.Decisions worth reviewing
activeandpast_duekeep the label, sincepast_dueis the grace period while Dodo retries a renewal. Every other status removes it./successonly gives the label. The redirect can arrive while the subscription is stillpending, so removing the label is left to the webhook.verifyUrl. Called through the Appwrite API, the function sees an internal address as its host, so it can't build its own URL. WithoutverifyUrl, the buyer returns straight tosuccessUrland the webhook provisions them. The demo pages pass their own/successURL.200. That covers a payment or subscription withoutmetadata.user_id, and a subscription whose Appwrite user was deleted. Dodo retries any non-2xx response for about 28 hours.getTableand, on404, creates the database and the table with its columns inline, 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.successUrl,failureUrlandverifyUrlagainst your own domains before going live.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/*.js,npm auditand prettier pass in both templates.standardwebhooksand a throwaway secret, and the Appwrite and Dodo calls are stubbed. They cover signature failures, a forged payload losing to the Dodo lookup,/successand the webhook running at the same time, both race directions on the label, and a deleted user./successand the order is listed on the first page load. The webhook and/successstored it exactly once./successfor a cancelled subscription left the label alone.user_idandsuccess_url) reaches both the payment and the subscription.Test deployments: payments and subscriptions.
Notes
rustlsadvisories (RUSTSEC-2026-0347 to 0349 and one more) inrust/starter. This PR doesn't touch Rust, so that needs its own fix on main.🤖 Generated with Claude Code