پرداخت با Stripe در Next.js بدون دردسر وبهوک: راه пошترایپ و روش دستی
نویسنده: آرش لطیفی
وبهوکهای Stripe ریشه دردسرهای پرداخت هستند: تکرار ایونت، ترتیب نامعین، سقف نرخ API. دو راهحل واقعی: کتابخانه opensource stripe-no-webhooks و روش sync-برنامهریزیشده. با کد Next.js و Postgres.
TL;DR — بیشتر باگهای پرداخت Stripe از وبهوک میآید: ایونت تکراری، ترتیب نامعین، اکسترکشن fail شدن سایلنت. کتابخانه opensource
stripe-no-webhooks(۲۰۲۶-۱۰ در HN) کل لایه وبهوک را حذف میکند و داده Stripe را مستقیم در Postgres شما sync میکند — یا میتوانید همین ایده را با cron خودتان پیاده کنید.
چرا وبهوک Stripe انقدر دردسر است
مستندات رسمی Stripe (docs.stripe.com/webhooks) خودش سه رفتار را تصریح میکند که هر کدام بهتنهایی منبع باگ است:
- تکرار ایونت: «Webhook endpoints might occasionally receive the same event more than once» — یعنی endpoint شما باید idempotent باشد؛ Stripe این را به عهده شما میگذارد.
- ریترای تا سه روز: در لایو مود Stripe با exponential backoff تا ۳ روز دوباره تلاش میکند. اگر endpoint طولانی down باشد، صف ریترای آبشاری برمیگردد.
- ریسپانس سریع قبل از منطق: باید قبل از هر منطق سنگین 2xx برگردانید («return a 200 response before updating a customer's invoice as paid») — یعنی عملا هر هندلر به یک صف داخلی نیاز دارد.
به اینها سقف نرخ API را هم اضافه کنید: piscator... ببخشید، نویسنده stripe-no-webhooks در ترد HN (news.ycombinator.com/item?id=46963177) اشاره کرد که Stripe API سقف ۱۰۰ درخواست در دقیقه دارد و برای اپهای واقعی این محدودیت حس میشود. نتیجه: هر پروژه Stripe یک لایه «زیرساخت وبهوک» اختصاصی میخواهد که ربطی به بیزنس ندارد.
راهحل ۱: کتابخانه stripe-no-webhooks
پروژه opensource (github.com/pretzelai/stripe-no-webhooks) که اواخر ۲۰۲۶ در Hacker News معرفی شد. رویکردش: به جای اینکه برای هر ایونت listener بنویسید، داده Stripe بهطور مداوم داخل Postgres خودتان sync میشود و اپ شما از دیتابیس خودش میخواند — همان رویکرد dj-stripe در دنیای Django.
بر اساس README مخزن، راهاندازی شش مرحله دارد:
پلانها در کد تعریف میشوند (فایل billing.config.ts)، نه دستی در داشبورد:
کلاینت بیلینگ با resolveUser به سیستم احراز هویت شما وصل میشود — مثلا با Clerk در Next.js:
چیزهایی که طبق README پشتیبانی میکند: اشتراک، اعتبار (credits)، کیف پول، بیلینگ مصرفی (trackUsage: true)، بیلینگ seat-based، جمع آپ/داونگرید پلن با هندل درست اعتبار، و کالبهکهای اختیاری مثل onSubscriptionCreated.
راهحل ۲: sync زمانبندیشده خودتان
اگر نمیخواهید به یک لایبرری وابسته باشید، همان ایده را سادهتر پیاده کنید:
| روش | چه کار میکند | مناسب برای |
|---|---|---|
| Webhook کلاسیک | endpoint عمومی + امضا + idempotency + صف داخلی | ایونتهای فوری (مثل بلاک کردن دسترسی لحظه لغو اشتراک) |
| Polling با cron | هر ۵-۱۵ دقیقه ایونتهای جدید از Stripe API را بکش و در Postgres ذخیره کن | سابسکریپشن، اینویس، پرتال — جاهایی که تاخیر چند دقیقهای مهم نیست |
| stripe-no-webhooks | همان polling، ولی + مدیریت پلن/اعتبار/کیف پول آماده | اپ SaaS با Next.js و Postgres که نمیخواهد زیرساخت بسازد |
الگوی polling فقط دو تکه کد میخواهد: یک route که با stripe.events.list({ created: { gte: lastSync } }) ایونتها را میخواند (ایونت پردازششده را با ID در جدول log ذخیره کنید — این همان idempotency است که Stripe از شما میخواهد)، و یک cron که آن route را صدا بزند. اگر از Next.js استفاده میکنید، میتوانید cron را با Vercel Cron یا حتی یک Worker خارجی ببندید.
نکته امنیتی از مستندات Stripe که در هر دو روش درست است: امضای وبهوک را با whsec و هدر Stripe-Signature روی raw body چک کنید — اگر body را قبل از verify پارس کنید، امضا fail میشود. و کد وضعیت 2xx را قبل از هر منطق سنگین برگردانید.
چه زمانی هنوز وبهوک لازم است؟
حذف کامل وبهوک برای همه جا درست نیست. این موارد هنوز وبهوک میخواهند:
- اقدام فوری: قطع دسترسی کاربر در لحظه
customer.subscription.deleted— polling چند دقیقهای تاخیر دارد. - مصرف لحظهای: جایی که به ازای هر ریکوئست کبارد میشود و باید real-time رصد شود.
ترکیب رایج و سالم: polling بهعنوان منبع حقیقت (source of truth) در Postgres + یک وبهوک سبک فقط برای ایونتهای حساس به زمان. اینطوری اگر وبهوک سوخت، داده از دست نمیرود — دفعه بعد polling جبران میکند.
چکلیست پیادهسازی
- [ ] endpoint وبهوک (اگر دارید): verify امضا روی raw body با
whsecقبل از هر چیز. - [ ] idempotency: ID هر ایونت پردازششده را ذخیره کن و قبل از پردازش چک کن — مستندات Stripe خودش این را best practice اعلام کرده.
- [ ] 2xx سریع، منطق سنگین بعدا: ریسپانس را قبل از آپدیت اینویس/دیتابیس برگردان.
- [ ] سقف نرخ ۱۰۰ rpm را جدی بگیر: bulk operationها را صف کن، نه مستقیم به API.
- [ ] وضعیت اشتراک را هرگز فقط از وبهوک استنتاج نکن — یک sync دورهیی برای reconcile داشته باش.
- [ ] مد لایو و sandbox جدا تست کن — ریترای sandbox (۳ بار در چند ساعت) با لایو (۳ روز) فرق دارد.
سوالات پرتکرار
Q: آیا stripe-no-webhooks جایگزین کامل وبهوک است؟
برای sync داده و مدیریت پلن/اعتبار بله — کتابخانه وبهوک را خودش هندل میکند (route آماده app/api/stripe/[...all]/route.ts میسازد). اما اگر منطق real-time حساسی داری (مثل قطع فوری دسترسی)، همان بخش را با کالبهک یا وبهوک سبک خودت اضافه کن.
Q: polling هر چند دقیقه یک بار امن است؟
برای سابسکریپشن و اینویس، ۵ تا ۱۵ دقیقه عملا کافی است — کاربر واقعی متوجه نمیشود. برای لغو اشتراک که باید فوری جلوی دسترسی را بگیرد، polling تنها کافی نیست؛ وبهوک یا چک سمت سرور روی هر request لازم است.
Q: اپ من Zarinpal دارد نه Stripe؛ این مطلب به چه دردش میخورد؟
الگوی معماری یکسان است: به جای کلید کردن وبهوک تامینکننده پرداخت، وضعیت تراکنشها را دورهیی با API verify خودت sync کن و در دیتابیس خودت source of truth بساز. وبهوک فقط برای سرعت، sync برای صحت.