nyxcore-systems
6 min read

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.

StripeBillingSaaSNext.jsTypeScriptPrismaWebhooksDeveloperJourneyLessonsLearned

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 introduced Plan and Subscription models to meticulously track our pricing tiers and user subscriptions. A subscription relation was added to the Tenant model, 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 with getStripeWebhookSecret().
  • 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: getPlans to display available subscriptions, getSubscription to fetch a user's current plan, createCheckoutSession to initiate a purchase, and createPortalSession for 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.1 to ensure we're on the cutting edge.
  • Production Deployment: After meticulous testing, the new plans and subscriptions tables were created on our production database (with a special workaround, detailed below!), the nyxCore Pro plan was inserted, and the entire system was deployed under commit 70ae1f0.

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 if STRIPE_SECRET_KEY was 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:push to apply our new Plan and Subscription models, Prisma unexpectedly wanted to drop our existing embedding vector column on the workflow_insights table. This is a known issue with Prisma's introspection and migration capabilities when dealing with certain custom types or extensions like pgvector. 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 plans and subscriptions tables directly via SQL using docker exec nyxcore-postgres-1 psql. This allowed us to safely introduce the new tables without risking our existing data. For future migrations involving pgvector, 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.subscription moved to invoice.parent.subscription_details.subscription.
    • subscription.current_period_start/end moved to subscription.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 CardDescription component in our card.tsx library, and the sonner package for beautiful toasts wasn't installed.
  • The Fix: Instead of getting sidetracked, we opted for pragmatism. We used our existing @/hooks/use-toast system for notifications and incorporated inline descriptions where CardDescription would 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.production on 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.production file and restart the nyxCore container!

What's Next for nyxCore?

With billing successfully deployed, our immediate focus shifts to:

  1. Finalizing Environment Variables: As mentioned, this is paramount for the billing page to function correctly.
  2. 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 in evaluations/page.tsx.
  3. 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.
  4. 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!