Hey IndieHackers!
I just launched the "Stealth Arsenal for Solopreneurs" (including an AI Idea Roaster!). While building the AI was super fun, integrating the newest Paddle Billing with Next.js 14 (App Router) and Firebase almost broke my spirit.
To save you from the same pain and a weekend of debugging, here are my top 3 technical struggles and the exact code snippets that fixed them.
🤬 1. The Next.js "Raw Body" Webhook Trap
Paddle sends a webhook when a user pays. To prevent scammers, you must verify the cryptographic Paddle-Signature. In Next.js App Router, it's a habit to parse payloads using await req.json().
Don't do it. Parsing the JSON alters the formatting slightly, which completely breaks Paddle's unmarshal function, causing endless "Signature Mismatch" errors.
The Fix: You must use await req.text() to grab the raw HTTP stream exactly as Paddle sent it.
typescript
// app/api/webhooks/paddle/route.ts
export async function POST(req: NextRequest) {
const signature = req.headers.get('paddle-signature') || '';
// DO NOT use req.json()! It manipulates the string.
const body = await req.text();
const secret = process.env.PADDLE_WEBHOOK_SECRET || '';
// Now it verifies perfectly
const eventData = await paddle.webhooks.unmarshal(body, secret, signature);
// ...
}
💥 2. The Firebase Admin Serverless Crash
When the webhook fires, I use the Firebase Admin SDK to add credits directly to the user's document in Firestore.
But on launch day, users signed up, the webhooks fired, and... my Next.js serverless function crashed. Why? Because the massive FIREBASE_SERVICE_ACCOUNT JSON string wasn't properly pasted into the Vercel Production Environment Variables. The webhook got a 200 OK from Paddle but adminDb silently failed to initialize, meaning zero credits were added to users.
The Fix: Always ensure your Service Account JSON is perfectly stringified in Vercel settings. Also, strictly implement the Singleton pattern for admin.initializeApp() so you don't get "App already exists" errors during Vercel cold starts.
🐢 3. React Strict Mode vs. The Checkout Modal
I used [@paddle](/paddle)/paddle-js for the frontend checkout UI. In Next.js development (React Strict Mode), useEffect fires twice. This caused Paddle to attempt to initialize the checkout modal overlay multiple times, causing weird UI glitches.
Worse, when checkout.completed fired, the modal wouldn't close gracefully because the React state hook was stale inside the callback.
The Fix: I wrapped the app in a global <PaddleProvider> singleton and used a dirty (but bulletproof) custom JS event window.dispatchEvent with a setTimeout to forcefully close paywalls without relying on React state synchronization.
typescript
// components/PaddleProvider.tsx
initializePaddle({
environment: 'production',
token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
eventCallback: function (data) {
if (data.name === "checkout.completed") {
// 1. Force close the Paddle checkout UI
if (window && (window as any).Paddle) {
(window as any).Paddle.Checkout.close();
}
// 2. Hacky but bulletproof way to bypass stale React states
// and unmount our custom Paywall modals globally
setTimeout(() => {
window.dispatchEvent(new Event('paddleCheckoutCompleted'));
}, 500);
}
}
})
Conclusion
If you're building a SaaS with Next.js + Paddle Billing + Firebase, I hope these snippets save your launch day.
Has anyone else fought with Paddle's new SDK? Let me know if you hit any other weird edge cases!