From 2ffccc699032fbf03e2e8dce8acb928d5323ac2c Mon Sep 17 00:00:00 2001 From: adityagarud Date: Wed, 8 Oct 2025 16:03:57 +0530 Subject: [PATCH 1/7] feat: add Express + MongoDB boilerplate to community projects and documentation --- community/projects.mdx | 5 + .../express-mongodb-boilerplate.mdx | 357 ++++++++++++++++++ docs.json | 1 + 3 files changed, 363 insertions(+) create mode 100644 developer-resources/express-mongodb-boilerplate.mdx diff --git a/community/projects.mdx b/community/projects.mdx index 7700fb7..ce153ab 100644 --- a/community/projects.mdx +++ b/community/projects.mdx @@ -49,6 +49,11 @@ Browse community projects by category. Each card links directly to the repositor + + Starter template integrating Dodo Payments with Express, MongoDB, and verified webhook handling. +
Ideal for beginners and intermediate developers. +

`Express` `MongoDB` `TypeScript` +
Starter boilerplate for integrating Dodo Payments quickly.
Includes example checkout flow, API wiring, and environment configuration. diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx new file mode 100644 index 0000000..54854d4 --- /dev/null +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -0,0 +1,357 @@ +--- +title: Express + MongoDB Boilerplate (Dodo Payments) +description: Production-ready starter to integrate Dodo Payments with Express and MongoDB, including webhooks and storage. +icon: "server" +--- + + + + Install official SDKs for Node, Python, and Go. + + + + Learn webhook delivery, retries, and verification. + + + +## Overview + +This boilerplate shows how to build an Express server with MongoDB and integrate Dodo Payments end-to-end: +- Create subscriptions and one-time payments +- Verify and handle webhooks using Standard Webhooks (`standardwebhooks`) +- Persist customers, payments, and subscription states in MongoDB +- Expose secure endpoints with environment-driven configuration + +## Prerequisites + +- Node.js 18+ +- MongoDB instance (Atlas or local) +- Dodo Payments API Key and Webhook Secret + +## Project Structure + +```bash +express-dodo/ + .env + src/ + app.ts + routes/ + payments.ts + subscriptions.ts + webhooks.ts + db/ + client.ts + models.ts + package.json + README.md +``` + +## Quickstart + +```bash +npm init -y +npm install express mongoose cors dotenv dodopayments standardwebhooks +npm install -D typescript ts-node @types/express @types/node @types/cors +npx tsc --init +``` + +`.env`: + +```bash +DODO_PAYMENTS_API_KEY=sk_test_xxx +DODO_WEBHOOK_SECRET=whsec_xxx +MONGODB_URI=mongodb+srv://:@/dodo?retryWrites=true&w=majority +PORT=3000 +``` + +## Database Setup + +```ts +// src/db/client.ts +import mongoose from 'mongoose'; + +export async function connectDB(uri: string) { + if (mongoose.connection.readyState === 1) return; + await mongoose.connect(uri, { dbName: 'dodo' }); +} +``` + +```ts +// src/db/models.ts +import mongoose from 'mongoose'; + +const CustomerSchema = new mongoose.Schema({ + customerId: { type: String, index: true }, + email: String, + name: String, +}); + +const PaymentSchema = new mongoose.Schema({ + paymentId: { type: String, index: true }, + status: String, + amount: Number, + currency: String, + customerId: String, + metadata: {}, +}); + +const SubscriptionSchema = new mongoose.Schema({ + subscriptionId: { type: String, index: true }, + status: String, + productId: String, + customerId: String, + currentPeriodEnd: Date, + metadata: {}, +}); + +export const Customer = mongoose.model('Customer', CustomerSchema); +export const Payment = mongoose.model('Payment', PaymentSchema); +export const Subscription = mongoose.model('Subscription', SubscriptionSchema); +``` + +## Express App + +```ts +// src/app.ts +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import { connectDB } from './db/client'; +import paymentsRouter from './routes/payments'; +import subsRouter from './routes/subscriptions'; +import webhooksRouter from './routes/webhooks'; + +const app = express(); +app.use(cors()); +// Only parse JSON for API routes; do not parse JSON for the webhook path +app.use('/api', express.json()); + +async function bootstrap() { + await connectDB(process.env.MONGODB_URI!); + + app.use('/api/payments', paymentsRouter); + app.use('/api/subscriptions', subsRouter); + app.use('/webhooks/dodo', webhooksRouter); + + const port = Number(process.env.PORT) || 3000; + app.listen(port, () => console.log(`Server listening on :${port}`)); +} + +bootstrap(); +``` + +## Payments Route + +```ts +// src/routes/payments.ts +import { Router } from 'express'; +import DodoPayments from 'dodopayments'; + +const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); +const router = Router(); + +router.post('/', async (req, res) => { + try { + const { + billing, + customer, + product_cart, + return_url, + metadata, + allowed_payment_method_types, + discount_code, + show_saved_payment_methods, + tax_id, + } = req.body; + + if (!billing || !customer || !product_cart) { + return res.status(400).json({ error: 'billing, customer, and product_cart are required' }); + } + + const payment = await client.payments.create({ + billing, + customer, + product_cart, + payment_link: true, + return_url, + metadata, + allowed_payment_method_types, + discount_code, + show_saved_payment_methods, + tax_id, + }); + + res.json(payment); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } +}); + +export default router; +``` + +## Subscriptions Route + +```ts +// src/routes/subscriptions.ts +import { Router } from 'express'; +import DodoPayments from 'dodopayments'; + +const router = Router(); +const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); + +router.post('/', async (req, res) => { + try { + const { + billing, + customer, + product_id, + quantity, + return_url, + metadata, + discount_code, + show_saved_payment_methods, + tax_id, + trial_period_days, + } = req.body; + + if (!billing || !customer || !product_id || !quantity) { + return res.status(400).json({ error: 'billing, customer, product_id, and quantity are required' }); + } + + const sub = await client.subscriptions.create({ + billing, + customer, + product_id, + quantity, + payment_link: true, + return_url, + metadata, + discount_code, + show_saved_payment_methods, + tax_id, + trial_period_days, + }); + + res.json(sub); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } +}); + +export default router; +``` + +## Webhooks (Verified) + +```ts +// src/routes/webhooks.ts +import { Router } from 'express'; +import { raw } from 'express'; +import { Subscription, Payment } from '../db/models'; +import { Webhook, type WebhookUnbrandedRequiredHeaders } from 'standardwebhooks'; + +const router = Router(); + +// Use Standard Webhooks with your secret +const webhook = new Webhook(process.env.DODO_WEBHOOK_SECRET as string); + +// Use raw body for signature verification (must not be pre-parsed by express.json()) +router.post('/', raw({ type: 'application/json' }), async (req, res) => { + try { + const rawBody = req.body.toString('utf8'); + + const headers: WebhookUnbrandedRequiredHeaders = { + 'webhook-id': (req.header('webhook-id') || '') as string, + 'webhook-signature': (req.header('webhook-signature') || '') as string, + 'webhook-timestamp': (req.header('webhook-timestamp') || '') as string, + }; + + await webhook.verify(rawBody, headers); + + const payload = JSON.parse(rawBody) as { + type: string; + data: any; + }; + + switch (payload.type) { + case 'subscription.active': { + const data = payload.data; + await Subscription.updateOne( + { subscriptionId: data.subscription_id }, + { + subscriptionId: data.subscription_id, + status: 'active', + productId: data.product_id, + customerId: data.customer?.customer_id, + currentPeriodEnd: data.current_period_end ? new Date(data.current_period_end) : undefined, + metadata: data.metadata || {}, + }, + { upsert: true } + ); + break; + } + case 'subscription.on_hold': { + const data = payload.data; + await Subscription.updateOne( + { subscriptionId: data.subscription_id }, + { status: 'on_hold' } + ); + break; + } + case 'payment.succeeded': { + const p = payload.data; + await Payment.updateOne( + { paymentId: p.payment_id }, + { + paymentId: p.payment_id, + status: 'succeeded', + amount: p.total_amount, + currency: p.currency, + customerId: p.customer?.customer_id, + metadata: p.metadata || {}, + }, + { upsert: true } + ); + break; + } + case 'payment.failed': { + const p = payload.data; + await Payment.updateOne( + { paymentId: p.payment_id }, + { status: 'failed' } + ); + break; + } + default: + // ignore unknown events + break; + } + + return res.json({ received: true }); + } catch (err: any) { + return res.status(400).json({ error: err.message }); + } +}); + +export default router; +``` + + +Ensure your Express app does not use `express.json()` on the webhook route, as it must read the raw body for signature verification. + + +## Deploy Notes + +- Use environment variables for secrets +- Prefer HTTPS for webhook endpoint +- Configure retry-safe handlers (idempotent writes) +- Add indexes on `paymentId`, `subscriptionId`, and `customerId` + +## Next Steps + +- Fork this page into a standalone GitHub template repository +- Add CI for linting and type checks +- Contribute the link to Community Projects + + diff --git a/docs.json b/docs.json index 7f9f6ca..227bfab 100644 --- a/docs.json +++ b/docs.json @@ -123,6 +123,7 @@ "pages": [ "developer-resources/checkout-session", "developer-resources/usage-based-billing-guide", + "developer-resources/express-mongodb-boilerplate", "developer-resources/integration-guide", "developer-resources/subscription-integration-guide", "developer-resources/subscription-upgrade-downgrade", From 65439a46b31bae355be6a669eeb9dd7fd0ef8864 Mon Sep 17 00:00:00 2001 From: adityagarud Date: Wed, 8 Oct 2025 16:14:20 +0530 Subject: [PATCH 2/7] feat: enhance TypeScript setup instructions for Express + MongoDB boilerplate --- .../express-mongodb-boilerplate.mdx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx index 54854d4..0a0bab2 100644 --- a/developer-resources/express-mongodb-boilerplate.mdx +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -55,6 +55,21 @@ npm install -D typescript ts-node @types/express @types/node @types/cors npx tsc --init ``` + +TypeScript default imports (e.g., `import express from 'express'`) require enabling interop flags. After running `npx tsc --init`, update `tsconfig.json`: + +```json +{ + "compilerOptions": { + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + } +} +``` + +This guide uses default imports and assumes these flags are enabled. + + `.env`: ```bash @@ -140,6 +155,30 @@ async function bootstrap() { bootstrap(); ``` + +Alternative (no tsconfig changes): If you prefer not to enable `esModuleInterop`, switch to namespace/compat imports and Router construction: + +```ts +// src/app.ts (no esModuleInterop) +import * as express from 'express'; +import * as cors from 'cors'; + +const app = express(); +app.use(cors()); +``` + +```ts +// src/routes/payments.ts (no esModuleInterop) +import * as express from 'express'; +import DodoPayments from 'dodopayments'; + +const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); +const router = express.Router(); +``` + +Both approaches are valid; the rest of this guide uses default imports with `esModuleInterop` enabled. + + ## Payments Route ```ts From b7ca3efadec9858491b5de89224a058a65dc49ac Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sat, 18 Oct 2025 12:06:58 +0530 Subject: [PATCH 3/7] fix: update raw body type in webhook route to accept all content types --- developer-resources/express-mongodb-boilerplate.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx index 0a0bab2..c0b8b45 100644 --- a/developer-resources/express-mongodb-boilerplate.mdx +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -296,7 +296,7 @@ const router = Router(); const webhook = new Webhook(process.env.DODO_WEBHOOK_SECRET as string); // Use raw body for signature verification (must not be pre-parsed by express.json()) -router.post('/', raw({ type: 'application/json' }), async (req, res) => { +router.post('/', raw({ type: '*/*' }), async (req, res) => { try { const rawBody = req.body.toString('utf8'); From 5646c37640cd6bd3c38014bba63858e84ba5781e Mon Sep 17 00:00:00 2001 From: adityagarud Date: Wed, 29 Oct 2025 23:53:47 +0530 Subject: [PATCH 4/7] feat: update Express + MongoDB boilerplate with new Dodo Payments integration and improved webhook handling --- .../express-mongodb-boilerplate.mdx | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx index c0b8b45..f75d841 100644 --- a/developer-resources/express-mongodb-boilerplate.mdx +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -4,6 +4,10 @@ description: Production-ready starter to integrate Dodo Payments with Express an icon: "server" --- + +Prefer starting from the ready-to-use template repository: yashranaway/express-dodo + + Install official SDKs for Node, Python, and Go. @@ -12,13 +16,16 @@ icon: "server" Learn webhook delivery, retries, and verification. + + Fork and start coding instantly. + ## Overview This boilerplate shows how to build an Express server with MongoDB and integrate Dodo Payments end-to-end: - Create subscriptions and one-time payments -- Verify and handle webhooks using Standard Webhooks (`standardwebhooks`) +- Verify and handle webhooks using `dodopayments-webhooks` - Persist customers, payments, and subscription states in MongoDB - Expose secure endpoints with environment-driven configuration @@ -50,7 +57,7 @@ express-dodo/ ```bash npm init -y -npm install express mongoose cors dotenv dodopayments standardwebhooks +npm install express mongoose cors dotenv dodopayments dodopayments-webhooks npm install -D typescript ts-node @types/express @types/node @types/cors npx tsc --init ``` @@ -138,11 +145,13 @@ import webhooksRouter from './routes/webhooks'; const app = express(); app.use(cors()); -// Only parse JSON for API routes; do not parse JSON for the webhook path +// Parse JSON for API routes; webhook route will also use express.json() app.use('/api', express.json()); async function bootstrap() { - await connectDB(process.env.MONGODB_URI!); + if (process.env.MONGODB_URI) { + await connectDB(process.env.MONGODB_URI); + } app.use('/api/payments', paymentsRouter); app.use('/api/subscriptions', subsRouter); @@ -186,11 +195,13 @@ Both approaches are valid; the rest of this guide uses default imports with `esM import { Router } from 'express'; import DodoPayments from 'dodopayments'; -const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); const router = Router(); router.post('/', async (req, res) => { try { + const apiKey = process.env.DODO_PAYMENTS_API_KEY; + if (!apiKey) return res.status(500).json({ error: 'DODO_PAYMENTS_API_KEY is not set' }); + const client = new DodoPayments({ bearerToken: apiKey }); const { billing, customer, @@ -237,10 +248,12 @@ import { Router } from 'express'; import DodoPayments from 'dodopayments'; const router = Router(); -const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); router.post('/', async (req, res) => { try { + const apiKey = process.env.DODO_PAYMENTS_API_KEY; + if (!apiKey) return res.status(500).json({ error: 'DODO_PAYMENTS_API_KEY is not set' }); + const client = new DodoPayments({ bearerToken: apiKey }); const { billing, customer, @@ -286,36 +299,22 @@ export default router; ```ts // src/routes/webhooks.ts import { Router } from 'express'; -import { raw } from 'express'; +import express from 'express'; import { Subscription, Payment } from '../db/models'; -import { Webhook, type WebhookUnbrandedRequiredHeaders } from 'standardwebhooks'; +import { DodopaymentsHandler } from 'dodopayments-webhooks'; const router = Router(); -// Use Standard Webhooks with your secret -const webhook = new Webhook(process.env.DODO_WEBHOOK_SECRET as string); - -// Use raw body for signature verification (must not be pre-parsed by express.json()) -router.post('/', raw({ type: '*/*' }), async (req, res) => { +router.post('/', express.json(), async (req, res) => { try { - const rawBody = req.body.toString('utf8'); - - const headers: WebhookUnbrandedRequiredHeaders = { - 'webhook-id': (req.header('webhook-id') || '') as string, - 'webhook-signature': (req.header('webhook-signature') || '') as string, - 'webhook-timestamp': (req.header('webhook-timestamp') || '') as string, - }; - - await webhook.verify(rawBody, headers); + const secret = process.env.DODO_WEBHOOK_SECRET; + if (!secret) return res.status(500).json({ error: 'DODO_WEBHOOK_SECRET is not set' }); + const handler = new DodopaymentsHandler({ signing_key: secret }); + const event = await handler.handle(req); - const payload = JSON.parse(rawBody) as { - type: string; - data: any; - }; - - switch (payload.type) { + switch (event.type) { case 'subscription.active': { - const data = payload.data; + const data = event.data as any; await Subscription.updateOne( { subscriptionId: data.subscription_id }, { @@ -331,7 +330,7 @@ router.post('/', raw({ type: '*/*' }), async (req, res) => { break; } case 'subscription.on_hold': { - const data = payload.data; + const data = event.data as any; await Subscription.updateOne( { subscriptionId: data.subscription_id }, { status: 'on_hold' } @@ -339,7 +338,7 @@ router.post('/', raw({ type: '*/*' }), async (req, res) => { break; } case 'payment.succeeded': { - const p = payload.data; + const p = event.data as any; await Payment.updateOne( { paymentId: p.payment_id }, { @@ -355,7 +354,7 @@ router.post('/', raw({ type: '*/*' }), async (req, res) => { break; } case 'payment.failed': { - const p = payload.data; + const p = event.data as any; await Payment.updateOne( { paymentId: p.payment_id }, { status: 'failed' } @@ -363,22 +362,21 @@ router.post('/', raw({ type: '*/*' }), async (req, res) => { break; } default: - // ignore unknown events break; } return res.json({ received: true }); } catch (err: any) { - return res.status(400).json({ error: err.message }); + return res.status(500).json({ error: err.message }); } }); export default router; ``` - -Ensure your Express app does not use `express.json()` on the webhook route, as it must read the raw body for signature verification. - + +Prefer using the community `dodopayments-webhooks` library for verified parsing and strong types. It works with standard `express.json()` and simplifies signature handling. See the repo: dodopayments-webhooks. + ## Deploy Notes From dbf5c8afc1589d1f1939cc34cb6a9e5314bb2f2c Mon Sep 17 00:00:00 2001 From: adityagarud Date: Wed, 29 Oct 2025 23:58:04 +0530 Subject: [PATCH 5/7] feat: reorganize documentation structure by grouping features into categories with icons --- docs.json | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs.json b/docs.json index 227bfab..3a99a7d 100644 --- a/docs.json +++ b/docs.json @@ -40,18 +40,31 @@ "features/transactions/disputes" ] }, - "features/account-summary-payout-wallet", + { + "group": "Payouts and Balances", + "icon": "wallet", + "pages": [ + "features/account-summary-payout-wallet", + "features/payouts/payout-structure" + ] + }, "features/customer-portal", "features/adaptive-currency", "features/license-keys", "features/storefront", "/features/digital-product-delivery", - "features/invoice-generation", + { + "group": "Account Management", + "icon": "user", + "pages": [ + "features/multi-brands", + "features/invoice-generation", + "features/team", + "miscellaneous/accounts" + ] + }, "features/b2b-payments", "features/customers", - "features/multi-brands", - "features/team", - "miscellaneous/accounts", "features/analytics-and-reporting", "features/purchasing-power-parity", "features/affiliates", From 6364390e28c3b8dc7375df86b5d3be51a191fd4a Mon Sep 17 00:00:00 2001 From: adityagarud Date: Thu, 30 Oct 2025 00:16:07 +0530 Subject: [PATCH 6/7] docs(express-mongodb): remove Next Steps section --- developer-resources/express-mongodb-boilerplate.mdx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx index f75d841..878988b 100644 --- a/developer-resources/express-mongodb-boilerplate.mdx +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -385,10 +385,4 @@ Prefer using the community `dodopayments-webhooks` library for verified parsing - Configure retry-safe handlers (idempotent writes) - Add indexes on `paymentId`, `subscriptionId`, and `customerId` -## Next Steps - -- Fork this page into a standalone GitHub template repository -- Add CI for linting and type checks -- Contribute the link to Community Projects - From 68ad90c4551bd580fc8fc50665d0efa824e4c8a8 Mon Sep 17 00:00:00 2001 From: adityagarud Date: Thu, 30 Oct 2025 00:22:29 +0530 Subject: [PATCH 7/7] docs(express-mongodb): clarify raw body requirement for webhook verification and library usage --- developer-resources/express-mongodb-boilerplate.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx index 878988b..70424c6 100644 --- a/developer-resources/express-mongodb-boilerplate.mdx +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -145,7 +145,9 @@ import webhooksRouter from './routes/webhooks'; const app = express(); app.use(cors()); -// Parse JSON for API routes; webhook route will also use express.json() +// Parse JSON for API routes; webhook routes MUST use raw body middleware +// (not express.json()) so the original bytes are preserved for signature +// verification. For security, apply raw body parsing only on the webhook path. app.use('/api', express.json()); async function bootstrap() { @@ -375,7 +377,7 @@ export default router; ``` -Prefer using the community `dodopayments-webhooks` library for verified parsing and strong types. It works with standard `express.json()` and simplifies signature handling. See the repo: dodopayments-webhooks. +Prefer using the community `dodopayments-webhooks` library for verified parsing and strong types. This library requires the raw request body to be preserved for signature verification — use a raw body middleware on the webhook route (or configure `express.json({ verify })` to capture the raw buffer) so verification can succeed. With raw-body-preserving middleware in place, the library enables verified parsing and strong types as advertised. See the repo: dodopayments-webhooks. ## Deploy Notes