Skip to content

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

Merged
adityaoberai merged 8 commits into
mainfrom
feat-dodo-payments-templates
Oct 19, 2026
Merged

adityaoberai merged 8 commits into
mainfrom
feat-dodo-payments-templates

Conversation

@adityaoberai

@adityaoberai adityaoberai commented Oct 8, 2026 •

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 Provisioning
node/payments-with-dodo-payments POST /checkout for a one-time product Stores the paid order as a row in an Appwrite table
node/subscriptions-with-dodo-payments POST /subscribe for a subscription product Adds or removes a subscriber label on the user

Both serve a demo page at GET /, styled after appwrite.io. Dependencies are dodopayments ^2.54.0, node-appwrite ^29.1.0 and prettier ^3.9.9. The demo pages load appwrite@^28.1.0, alpinejs@^3.17.4 and Inter from jsDelivr.

How a purchase is verified

Two paths provision the user, and either one is enough.

  1. POST /webhook checks the Standard Webhooks signature and answers 401 when it doesn't match. It then looks the payment or subscription up in Dodo Payments instead of trusting the event payload.
  2. GET /success is where Dodo Payments returns the buyer when the checkout request sets verifyUrl. It looks the payment or subscription up the same way, provisions the user, and redirects to successUrl.

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

  • Access follows the subscription's current status in Dodo Payments. active and past_due keep the label, since past_due is the grace period while Dodo retries a renewal. Every other status removes it.
  • Overlapping runs can't leave a stale label. A webhook and the redirect, or two webhooks, can run at once. After each label update, the function looks the subscription up again and applies the newer status if it changed in the meantime.
  • /success only gives the label. The redirect can arrive while the subscription is still pending, so removing the label is left to the webhook.
  • The caller passes verifyUrl. Called through the Appwrite API, the function sees an internal address as its host, so it can't build its own URL. Without verifyUrl, the buyer returns straight to successUrl and the webhook provisions them. The demo pages pass their own /success URL.
  • Events that can never succeed get a 200. That covers a payment or subscription without metadata.user_id, and a subscription whose Appwrite user was deleted. Dodo retries any non-2xx response for about 28 hours.
  • Setup runs before the first row. The function calls getTable and, on 404, creates the database and the table with its columns inline, 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.
  • The redirect URLs come from the caller. The READMEs say to check successUrl, failureUrl and verifyUrl against your own domains before going live.

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, npm audit and prettier pass in both templates.
  • Mock runs of the default export: 14 cases for payments and 18 for subscriptions. Events are signed with standardwebhooks and a throwaway secret, and the Appwrite and Dodo calls are stubbed. They cover signature failures, a forged payload losing to the Dodo lookup, /success and the webhook running at the same time, both race directions on the label, and a deleted user.
  • Live end to end on Appwrite Cloud and Dodo test mode, with the 4242 test card driven through the real checkout in a headless browser:
    • Payments: the buyer returns through /success and the order is listed on the first page load. The webhook and /success stored it exactly once.
    • Subscriptions: the page shows "Subscribed" on first load. Cancelling the subscription in Dodo removed the label within seconds, and /success for a cancelled subscription left the label alone.
    • Checkout metadata (user_id and success_url) reaches both the payment and the subscription.

Test deployments: payments and subscriptions.

Notes

  • The Rust audit job fails on four new rustls advisories (RUSTSEC-2026-0347 to 0349 and one more) in rust/starter. This PR doesn't touch Rust, so that needs its own fix on main.
  • The workflow on main regenerates the root README table, so this PR doesn't touch it.

🤖 Generated with Claude Code

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 S · Ready to merge

The incremental changes correctly handle deleted Appwrite users and document the acknowledgement behavior; no actionable defects were found.

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.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 10
File Change
node/payments-with-dodo-payments/README.md Documents that payments without user metadata are acknowledged.
node/payments-with-dodo-payments/.gitignore, node/payments-with-dodo-payments/.prettierrc.json Adds template-specific ignore and formatting configuration.
node/payments-with-dodo-payments/package.json, node/payments-with-dodo-payments/package-lock.json Defines the payments template dependencies and locked versions.
node/payments-with-dodo-payments/src/appwrite.js, node/payments-with-dodo-payments/src/dodopayments.js, node/payments-with-dodo-payments/src/main.js, node/payments-with-dodo-payments/src/utils.js Implements Appwrite order storage, Dodo checkout and webhook handling, payment verification, and shared helpers.
node/payments-with-dodo-payments/static/index.html Adds a demo UI for registration, checkout, and viewing orders.
node/subscriptions-with-dodo-payments/README.md Documents subscription status handling and acknowledgement of events for missing or deleted users.
node/subscriptions-with-dodo-payments/.gitignore, node/subscriptions-with-dodo-payments/.prettierrc.json Adds template-specific ignore and formatting configuration.
node/subscriptions-with-dodo-payments/package.json, node/subscriptions-with-dodo-payments/package-lock.json Defines the subscriptions template dependencies and locked versions.
node/subscriptions-with-dodo-payments/src/appwrite.js, node/subscriptions-with-dodo-payments/src/dodopayments.js, node/subscriptions-with-dodo-payments/src/main.js, node/subscriptions-with-dodo-payments/src/utils.js Implements subscription checkout, signed webhook processing, status-based label updates, redirect verification, and shared helpers; deleted-user 404s are acknowledged.
node/subscriptions-with-dodo-payments/static/index.html Adds a demo UI for registration and starting a subscription.

Reviewed the commits since 1f028b3 · 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
Comment thread node/payments-with-dodo-payments/src/main.js
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 thread node/payments-with-dodo-payments/src/main.js Outdated
adityaoberai and others added 2 commits October 16, 2026 01:31
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>

@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
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>

@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 S · Looks good to merge. Summary

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>
@adityaoberai

Copy link
Copy Markdown
Contributor Author

@atharvadeosthale ready for another look. Since your review:

  • Both webhooks look the payment or subscription up in Dodo Payments instead of trusting the payload (f6b89d4).
  • GET /success verifies the same way when Dodo returns the buyer, so either path provisions the user. The caller passes verifyUrl, and for subscriptions /success only ever adds the label (2942b77).
  • After each label update, the function looks the subscription up again and applies the newer status if it changed, so overlapping runs can't leave a stale label (1f028b3).
  • Events for deleted Appwrite users are acknowledged instead of retried for 28 hours (81eaa3d).

Both templates are tested end to end against Dodo test mode with the real checkout. The details are in the description.

@adityaoberai
adityaoberai merged commit cb701a8 into main Oct 19, 2026
27 of 28 checks passed
@adityaoberai
adityaoberai deleted the feat-dodo-payments-templates branch October 19, 2026 12:54
NancyWei123 pushed a commit to NancyWei123/appwrite that referenced this pull request Oct 19, 2026
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>
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