9:00 PM — Thứ Hai.
Màn hình VS Code sáng rực. Tôi vừa dùng Claude Code dựng xong cái lõi cho con SaaS mới — một tool quản lý subscription cho các creator Việt Nam. Next.js chạy mượt. Database schema gọn gàng. Auth flow hoạt động. Landing page đã có.
Cảm giác lúc đó? Phê. Tôi tự nhủ: “Phen này launch được rồi. Mai gắn payment vào là xong.”
“Chỉ cần gắn payment vào là xong.”
Nếu bạn là indie hacker và đã từng nói câu này — bạn biết chuyện gì sắp xảy ra.
Vực Thẳm Mang Tên Stripe Docs
Tôi mở tài liệu Stripe. 50 cái tabs mở ra trong 10 phút đầu.
Checkout Sessions. Webhooks. Product IDs. Price API. Customer Portal. Billing Meter. Subscription Lifecycle. Invoice Events.
Tôi là Software Engineer với hơn 5 năm kinh nghiệm. Tôi không sợ code khó. Nhưng tôi ghét — ghét thật sự — những thứ lặp đi lặp lại một cách nhàm chán mà sai một bước là vỡ trận.
Và payment integration chính xác là thứ đó.
Nút “Pay” Không Phải Là Một Nút
Đây là thứ không ai nói cho bạn khi bạn bắt đầu build SaaS: nút “Pay” không phải là một nút. Nó là một hệ thống.
Để cái nút đó hoạt động, tôi phải:
1. Setup Stripe CLI để bắt webhook từ local.
stripe listen --forward-to localhost:3000/api/webhooks/stripeNghe đơn giản? Đúng, cho đến khi bạn nhận ra CLI cần login, login cần API key, API key có test mode và live mode, và nếu nhầm mode thì mọi thứ chạy nhưng không có event nào được gửi. Bạn ngồi debug 40 phút để phát hiện mình đang dùng sai key.
2. Viết webhook handler — nơi mọi thứ thực sự xảy ra.
Thanh toán thành công? Cập nhật database. Thẻ hết hạn? Gửi email nhắc. Subscription bị cancel? Revoke access. Refund? Hoàn lại credits.
Mỗi event là một if block. Mỗi if block cần logic riêng. Mỗi logic cần test riêng.
// Đây mới chỉ là phần checkout.session.completed// Còn invoice.paid, invoice.payment_failed,// customer.subscription.updated,// customer.subscription.deleted...// Mỗi cái là một câu chuyện riêng.Tôi đếm được 12 loại event cần handle cho một subscription flow cơ bản. Mười hai.
3. Cấu hình biến môi trường.
STRIPE_SECRET_KEY=sk_test_...STRIPE_PUBLISHABLE_KEY=pk_test_...STRIPE_WEBHOOK_SECRET=whsec_...STRIPE_PRICE_ID_MONTHLY=price_...STRIPE_PRICE_ID_YEARLY=price_...Năm biến. Sai một ký tự — không có error message rõ ràng. Stripe chỉ trả về 400 Bad Request hoặc tệ hơn — 200 OK nhưng không tạo session. Bạn không biết mình sai ở đâu.
Ngày Thứ Hai — Webhook Hell
Thứ Ba. Tôi bắt đầu viết webhook handler.
Vấn đề đầu tiên: signature verification. Stripe gửi event kèm signature để bạn verify đó là event thật, không phải ai đó giả mạo. Logic verify này khác nhau giữa App Router và Pages Router trong Next.js. Tôi dùng App Router. Tài liệu ví dụ của Stripe dùng Pages Router.
Copy-paste không chạy. Stack Overflow answers cũ 2 năm. Blog posts dùng library version khác.
Tôi mất 3 tiếng chỉ để parse được cái request body đúng cách.
Vấn đề thứ hai: idempotency. Stripe có thể gửi cùng một event nhiều lần. Nếu bạn không handle trùng lặp, user trả tiền một lần nhưng database ghi nhận hai lần. Hoặc tệ hơn — gửi hai email xác nhận. Hoặc tệ hơn nữa — tạo hai subscription records.
Tôi chưa từng nghĩ đến trường hợp này cho đến khi nó xảy ra trên staging.
Ngày Thứ Ba — Cái Giá Thực Sự
Thứ Tư. Ngày thứ ba.
Webhook handler đã chạy. Database cập nhật đúng. Nhưng tôi chưa xử lý:
- Customer Portal — nơi user tự cancel/upgrade subscription
- Proration — tính tiền khi user upgrade giữa chu kỳ
- Trial periods — cho dùng thử 7 ngày trước khi charge
- Tax calculation — thuế khác nhau theo quốc gia
- Failed payment retry — Stripe tự retry nhưng bạn cần track trạng thái
Mỗi thứ trong danh sách trên là thêm 2-4 tiếng. Và tôi chưa tính testing.
Nhưng thứ giết tôi không phải khối lượng code. Mà là context switching. Stripe Dashboard để tạo product. Quay về VS Code viết handler. Nhảy sang terminal chạy CLI. Mở browser test checkout. Quay lại Dashboard kiểm tra event log. Lại VS Code sửa bug.
Mỗi lần nhảy là mất 5-10 phút để “load lại” đầu. Cái flow state mà tôi có lúc build feature? Biến mất hoàn toàn. Payment integration không chỉ tốn thời gian — nó giết chết khả năng tập trung.
Tôi ngồi nhìn VS Code lúc 11:30 PM thứ Tư. Code chính — cái lõi mà tôi hào hứng build — đã hoàn thành từ tối thứ Hai. Ba ngày qua tôi không viết thêm một dòng feature nào. Ba ngày chỉ để cho cái nút “Pay $9/month” hoạt động.
Đây là lúc tôi nghĩ đến việc bỏ cuộc.
Không Phải Lần Đầu
Và đây là phần đau nhất: đây không phải lần đầu tiên.
Dự án SaaS thứ nhất — một tool analytics nhỏ. Tôi mất 4 ngày cho payment. Launch muộn 1 tuần. Momentum mất. Dự án chết.
Dự án thứ hai — một marketplace cho freelancers Việt Nam. Lần này tôi “khôn hơn”, copy code payment từ dự án trước. Nhưng dự án trước dùng Pages Router, dự án mới dùng App Router. Copy-paste ra bug. Mất thêm 2 ngày sửa. Rồi phát hiện Stripe đã deprecate một API mà code cũ dùng. Viết lại từ đầu.
Ba dự án. Mỗi lần đều cùng một pattern:
Tuần 1: Build feature thần tốc nhờ AITuần 2: Kẹt cứng ở payment integrationTuần 3: Kiệt sức, mất momentum, bỏ cuộcVấn đề không phải ở Stripe. Stripe là một sản phẩm tuyệt vời. API được thiết kế tốt. Documentation đầy đủ. Đội ngũ support responsive.
Vấn đề là ở boilerplate. Là ở việc phải viết đi viết lại cùng một đống code webhook, checkout, customer portal — mỗi lần cho mỗi dự án. Là ở việc mỗi lần đều mắc những lỗi mới vì context đã thay đổi — framework version mới, API deprecated, pattern khác.
AI giúp tôi code feature trong vài giờ. Nhưng AI không giúp tôi skip được 12 webhook events cần handle, 5 ENV vars cần đúng, và 20 edge cases cần test.
Câu Hỏi Mà Tôi Ước Ai Đó Hỏi Tôi Sớm Hơn
Vào 11:30 PM thứ Tư, trước khi tôi đóng laptop, một câu hỏi hiện lên trong đầu:
“Nếu việc này lặp lại mỗi dự án — tại sao mình vẫn làm bằng tay?”
Tôi build feature bằng AI. Tôi generate test bằng AI. Tôi viết docs bằng AI. Nhưng payment — thứ phức tạp nhất, nhàm chán nhất, dễ lỗi nhất — tôi vẫn ngồi viết tay từng dòng webhook handler.
Không phải vì tôi thích. Mà vì tôi chưa tìm được cách nào khác.
Cho Đến Khi Tôi Tìm Được
3 ngày. Đó là thời gian tôi mất cho cổng thanh toán ở dự án thứ 3.
Ở dự án thứ 4, tôi mất 15 phút.
Không phải vì tôi giỏi hơn. Không phải vì Stripe đơn giản hơn. 15 phút đó không phải để copy-paste code cũ — mà là 15 phút để Claude Code tự hiểu cấu trúc dự án và setup toàn bộ payment từ A-Z, nhờ một thứ mà tôi vừa “huấn luyện” xong.
Bài sau tôi sẽ kể chính xác thứ đó là gì.
Nếu bạn đang ở đúng cái 11:30 PM thứ Tư đó — đang nhìn webhook handler dở dang và tự hỏi liệu có đáng không — thì bạn không cô đơn. Và có lối thoát.
Bạn muốn là người đầu tiên trải nghiệm giải pháp “15 phút” đó? Đăng ký tại đây để nhận thông báo khi bài tiếp theo lên sóng.
→ Bài 2: Giải pháp End-to-End mà tôi ước có từ dự án đầu tiên (coming soon)