Billing Goes Live! Navigating Stripe v20 and Prisma Pitfalls on the Road to nyxCore Pro
A deep dive into integrating Stripe subscriptions into nyxCore, covering schema design, webhook handling, and overcoming common development hurdles like environment variable management and Prisma migrations.
It's a landmark day for nyxCore! We've just pushed a significant update to production: full Stripe billing integration. This isn't just a new feature; it's the heartbeat of our SaaS, enabling subscription management and paving the way for our Pro plan. While the finish line feels great, the journey was a classic developer's tale of overcoming unexpected challenges and celebrating hard-won victories.
Our primary goal for this session was ambitious: get Stripe billing (specifically, subscription management) fully operational. The good news? It's deployed, it's live, and it's ready for our users.
The Blueprint: Bringing Billing to Life
Implementing a robust billing system touches almost every layer of your application. Here's a breakdown of what went into getting nyxCore's Stripe integration deployed:
- Database Schema (
prisma/schema.prisma): We introducedPlanandSubscriptionmodels to meticulously track our pricing tiers and user subscriptions. Asubscriptionrelation was added to theTenantmodel, linking users directly to their active plans. - Stripe SDK Initialization (
src/lib/stripe.ts): To interact with Stripe's powerful API, we set up a lazy-initialized singleton (getStripe()). This pattern proved crucial, as you'll soon read in our "Lessons Learned" section. We also secured our webhook handling withgetStripeWebhookSecret(). - Server-Side Logic (
src/server/services/billing-service.ts): This is the engine room. It orchestrates the creation of Stripe Checkout sessions (for new subscriptions), Portal sessions (for existing subscribers to manage their plans), and robust webhook event handling to keep our database in sync with Stripe's state. We're leveraging the latest Stripe v20 API, which came with its own set of surprises! - API Endpoints (
src/server/trpc/routers/billing.ts): We exposed four essential tRPC procedures:getPlansto display available subscriptions,getSubscriptionto fetch a user's current plan,createCheckoutSessionto initiate a purchase, andcreatePortalSessionfor subscription management. - Webhook Listener (
src/app/api/v1/webhooks/stripe/route.ts): A dedicated endpoint to receive real-time updates from Stripe, complete with crucial signature verification to ensure data integrity and security. - User Interface (
src/app/(dashboard)/dashboard/settings/billing/page.tsx): A sleek new billing dashboard where users can view their current plan, upgrade, or manage their subscription. - Dependencies: We brought in
stripe@20.4.1to ensure we're on the cutting edge. - Production Deployment: After meticulous testing, the new
plansandsubscriptionstables were created on our production database (with a special workaround, detailed below!), thenyxCore Proplan was inserted, and the entire system was deployed under commit70ae1f0.
Navigating the Treacherous Waters: Lessons Learned
No significant feature launch comes without its share of head-scratching moments. Here's a peek into the "pain points" that turned into valuable lessons:
1. The Environment Variable Build-Time Trap
-
The Challenge: Initially, I tried to initialize the Stripe SDK at the top level of
src/lib/stripe.ts, throwing an error ifSTRIPE_SECRET_KEYwas missing. This is a common pattern for ensuring critical environment variables are set. However, our Docker build process failed during Next.js's page data collection step because these runtime environment variables weren't available during the build phase. -
The Fix: We switched to a lazy initialization pattern using a function (
getStripe()). This ensures the Stripe SDK is only initialized (and thus, the environment variable check only occurs) when it's actually needed at runtime, after the environment variables have been loaded into the container.typescript// Before (failed during Docker build) // const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { ... }); // After (lazy initialization for runtime safety) let stripeSingleton: Stripe | undefined; export function getStripe() { if (!stripeSingleton) { const secretKey = process.env.STRIPE_SECRET_KEY; if (!secretKey) { throw new Error("STRIPE_SECRET_KEY is not set."); } stripeSingleton = new Stripe(secretKey, { apiVersion: '2020-08-27', // Or your preferred API version // ... other options }); } return stripeSingleton; }
2. Prisma's pgvector Dilemma
- The Challenge: When trying to run
npm run db:pushto apply our newPlanandSubscriptionmodels, Prisma unexpectedly wanted to drop our existingembeddingvector column on theworkflow_insightstable. This is a known issue with Prisma's introspection and migration capabilities when dealing with certain custom types or extensions likepgvector. Dropping that column would have meant data loss! - The Fix: We bypassed Prisma's migration engine for this specific change. Instead, we manually created the
plansandsubscriptionstables directly via SQL usingdocker exec nyxcore-postgres-1 psql. This allowed us to safely introduce the new tables without risking our existing data. For future migrations involvingpgvector, we'll need a more robust strategy, potentially involving custom SQL migration scripts.
3. Stripe API v20 Breaking Changes
- The Challenge: Upgrading to Stripe API v20 introduced some subtle but impactful breaking changes, particularly in how webhook events structure their data. For example:
invoice.subscriptionmoved toinvoice.parent.subscription_details.subscription.subscription.current_period_start/endmoved tosubscription.items.data[0].current_period_start/end.
- The Fix: Careful debugging and consulting the Stripe API documentation were key. We had to update our webhook event handlers to correctly parse the new data structures, ensuring our database stayed perfectly synchronized with Stripe's state. This highlights the importance of thorough testing with new API versions!
4. UI/UX Polish - Pragmatism Prevails
- The Challenge: While building the billing UI, we noticed a couple of minor omissions: we didn't have a
CardDescriptioncomponent in ourcard.tsxlibrary, and thesonnerpackage for beautiful toasts wasn't installed. - The Fix: Instead of getting sidetracked, we opted for pragmatism. We used our existing
@/hooks/use-toastsystem for notifications and incorporated inline descriptions whereCardDescriptionwould typically be used. Sometimes, "good enough for now" is the right call to maintain momentum on core features.
5. The Critical Environment Variable Deployment Lag
- The Challenge (Pending): This is our final, critical hurdle. While the code is deployed, the Stripe environment variables (
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,NEXT_PUBLIC_APP_URL) need to be added to/opt/nyxcore/.env.productionon the server and the container restarted. Until then, the billing page will error with "STRIPE_SECRET_KEY not set." Our team member mentioned they added them to Git Secrets, but they aren't yet available in the container's runtime environment. - Immediate Next Step: Get those environment variables into the production server's
.env.productionfile and restart the nyxCore container!
What's Next for nyxCore?
With billing successfully deployed, our immediate focus shifts to:
- Finalizing Environment Variables: As mentioned, this is paramount for the billing page to function correctly.
- Enhancing Persona Evaluations: We're looking to give users more control over their persona evaluations by allowing explicit provider and model selection, moving beyond the current auto-fallback mechanism. This involves updates to
persona-evaluator.ts,personas.ts(tRPC router), and the UI inevaluations/page.tsx. - Expanding Stripe Plans: Once the current billing flow is fully verified, we'll introduce additional plans, including a Free tier and an Enterprise tier, to cater to a wider range of users.
- Implementing Feature Gating: With plans in place, the next logical step is to wire up feature flags based on a user's subscription plan, unlocking advanced capabilities for our Pro users.
This session was a testament to the complexities and rewards of building a SaaS product. We're thrilled to have billing live, powering the next chapter of nyxCore, and we're excited for what's to come!