TypeScript Architecture Foundation

Trying to build a SaaS without proper TypeScript setup? Yeah, that's how you end up refunding someone $2,147 when their plan was supposed to be $21/month. Turns out someone passed cents as an integer instead of the price ID string. TypeScript would've caught that stupid type conversion bug immediately, but we were too clever for types back then.

Supabase Architecture

Anyway, here's the thing about TypeScript in this stack - it helps you model the relationships between all these services without your data getting fucked up between API calls.

Why TypeScript Matters for Payment Integration

Payment processing will ruin your life if you get it wrong. One typo and you're either losing money or overcharging customers. Stripe's TypeScript SDK is decent, but it won't save you from yourself. The Stripe API reference has types, but they're not foolproof. I've seen payment processing best practices guides that completely ignore the edge cases that break at 2am.

Consider this common mistake:

// ❌ This will silently fail and you'll spend 2 hours figuring out
// that you passed "price_free" instead of a real price ID
const createSubscription = async (customerId: string, price: string) => {
  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price }],
  });
  // Ask me how I know this breaks in production
};

Here's what actually works when you need to not bankrupt yourself:

// ✅ Type-safe with proper validation
import { z } from 'zod';

const CreateSubscriptionSchema = z.object({
  customerId: z.string().min(1),
  priceId: z.string().startsWith('price_'),
});

type CreateSubscriptionInput = z.infer<typeof CreateSubscriptionSchema>;

const createSubscription = async (input: CreateSubscriptionInput) => {
  const { customerId, priceId } = CreateSubscriptionSchema.parse(input);
  
  const subscription = await stripe.subscriptions.create({
    customer: customerId,
    items: [{ price: priceId }],
  });
  
  return subscription;
};

TypeScript Logo

Database Type Generation Strategy

Supabase generates TypeScript types from your database schema, which is great until you change a column name and your entire frontend explodes. Your types ARE the contract - mess with the database without regenerating types and you're debugging "Property 'user_id' does not exist" errors on Sunday morning. The Supabase CLI documentation shows local development setup, but the Supabase TypeScript guide glosses over how this breaks in real projects.

Generate types regularly during development:

npx supabase gen types typescript --project-id your-project-id > types/database.types.ts

When this fails (and it fucking will), you'll get some bullshit error like "socket hang up" or "connection refused."

Just run it again. The Supabase CLI is temperamental as hell - sometimes it works, sometimes it doesn't, no one knows why.

Critical pattern - separate generated types from application types:

// types/database.types.ts (generated)
export interface Database {
  public: {
    Tables: {
      users: {
        Row: {
          id: string;
          email: string;
          created_at: string;
        };
        Insert: {
          id?: string;
          email: string;
          created_at?: string;
        };
        Update: {
          id?: string;
          email?: string;
          created_at?: string;
        };
      };
    };
  };
}

// types/application.types.ts (your business logic types)
import { Database } from './database.types';

export type UserRow = Database['public']['Tables']['users']['Row'];
export type CreateUserInput = Database['public']['Tables']['users']['Insert'];

export interface UserProfile extends UserRow {
  subscription_status: 'active' | 'canceled' | 'past_due';
  stripe_customer_id: string;
}

Next.js 15 App Router Integration Patterns

Next.js Logo

Next.js 15 made Request APIs async, breaking every cookies() and headers() call you've ever written. Now you need await everywhere or your build fails with "cookies is not a function" errors.

The migration broke every auth check I'd written because apparently backwards compatibility is for chumps. Spent a solid day just adding await keywords everywhere like some kind of TypeScript zombie.

The Next.js TypeScript documentation tries to help, but good luck migrating a real app. The App Router documentation assumes you're starting fresh. For auth, the Supabase Next.js Auth guide and middleware docs will get you started, then you'll spend a week fixing edge cases.

Server Components with proper TypeScript:

// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const supabase = await createClient();
  
  const {
    data: { user },
    error
  } = await supabase.auth.getUser();

  if (!user || error) {
    redirect('/auth/login');
  }

  // TypeScript knows user exists here
  return <DashboardContent user={user} />;
}

interface DashboardContentProps {
  user: User; // Import from @supabase/supabase-js
}

function DashboardContent({ user }: DashboardContentProps) {
  // Fully typed component
  return <div>Welcome, {user.email}</div>;
}

Server Actions with validation:

// app/actions/subscription.ts
'use server';

import { z } from 'zod';
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';

const UpdateSubscriptionSchema = z.object({
  priceId: z.string().startsWith('price_'),
  customerId: z.string().startsWith('cus_'),
});

export async function updateSubscription(
  prevState: any,
  formData: FormData
) {
  const supabase = await createClient();
  
  const rawData = {
    priceId: formData.get('priceId'),
    customerId: formData.get('customerId'),
  };

  const result = UpdateSubscriptionSchema.safeParse(rawData);
  
  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
    };
  }

  try {
    // Type-safe database and Stripe operations
    const { data: user } = await supabase.auth.getUser();
    if (!user.user) {
      redirect('/auth/login');
    }

    const subscription = await stripe.subscriptions.create({
      customer: result.data.customerId,
      items: [{ price: result.data.priceId }],
    });

    // Update database with new subscription
    const { error } = await supabase
      .from('user_subscriptions')
      .upsert({
        user_id: user.user.id,
        stripe_subscription_id: subscription.id,
        status: subscription.status,
        current_period_end: new Date(subscription.current_period_end * 1000),
      });

    if (error) {
      throw error;
    }

    return { success: true };
  } catch (error) {
    return {
      errors: {
        _form: ['Failed to update subscription'],
      },
    };
  }
}

Full Stack Architecture

Payment Processing Type Safety

Stripe Logo

Stripe webhooks are a TypeScript nightmare. Everything comes in as any and you're back to JavaScript-style debugging.

Found this out the hard way when Stripe API version 2023-10-16 changed their subscription object structure - suddenly our webhook handler started throwing "Cannot read property 'current_period_end' of undefined" because they moved it into a nested object. Three hours of debugging later, I realized our validation was based on the old payload format.

Their documentation shows these perfect examples but conveniently skips the part where webhooks arrive out of order, duplicate, or just randomly missing fields. Zod documentation helps validate webhook event types, but you'll still miss edge cases.

Stripe Webhook Flow

Type-safe webhook processing:

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { z } from 'zod';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Define webhook event schemas
const CustomerSubscriptionUpdatedSchema = z.object({
  id: z.string(),
  object: z.literal('subscription'),
  customer: z.string(),
  status: z.enum(['active', 'canceled', 'incomplete', 'past_due', 'unpaid']),
  current_period_end: z.number(),
  current_period_start: z.number(),
  items: z.object({
    data: z.array(z.object({
      price: z.object({
        id: z.string(),
        unit_amount: z.number(),
      }),
    })),
  }),
});

type SubscriptionUpdated = z.infer<typeof CustomerSubscriptionUpdatedSchema>;

export async function POST(request: NextRequest) {
  const body = await request.text();
  const headersList = await headers();
  const signature = headersList.get('stripe-signature');

  if (!signature) {
    return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
  }

  try {
    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case 'customer.subscription.updated':
        await handleSubscriptionUpdated(event.data.object);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json(
      { error: 'Webhook handler failed' },
      { status: 400 }
    );
  }
}

async function handleSubscriptionUpdated(subscription: any) {
  // Validate the webhook data
  const result = CustomerSubscriptionUpdatedSchema.safeParse(subscription);
  
  if (!result.success) {
    console.error('Invalid subscription data:', result.error);
    return;
  }

  const validatedSubscription = result.data;

  // Type-safe database operations
  const supabase = createServiceRoleClient();
  
  const { error } = await supabase
    .from('user_subscriptions')
    .update({
      status: validatedSubscription.status,
      current_period_end: new Date(validatedSubscription.current_period_end * 1000),
      updated_at: new Date().toISOString(),
    })
    .eq('stripe_subscription_id', validatedSubscription.id);

  if (error) {
    console.error('Database update error:', error);
    throw error;
  }
}

This catches Stripe's bullshit API changes before they cost you money. Way better than finding out from angry customer emails.

Next up: authentication patterns that won't leave you debugging "user is null" errors in production.

Advanced Authentication & Database Integration

OK, personal rant over. Here's how I handle user-customer relationships without losing my mind. I spent three weeks figuring this shit out when my first SaaS launched and customers were getting orphaned Stripe records everywhere.

User-Customer Relationship Modeling

Here's the thing - every user needs exactly one Stripe customer. Fuck this up and you get orphaned records everywhere. I learned this the hard way when a failed webhook left 50 users without billing records and I had to manually reconcile everything.

You need to understand Supabase Auth or you'll be debugging sessions forever. The Row Level Security docs are actually decent, and the Stripe Customer API is straightforward once you stop fighting it. Check out the Supabase database design guide and PostgreSQL foreign keys if you want to do this right.

Your foreign keys are what save your ass when webhooks fail and users delete accounts at the worst possible time.

Database Schema

Database schema with proper TypeScript types:

// Database migrations (run via Supabase CLI)
-- Create users table that extends auth.users
create table public.user_profiles (
  id uuid references auth.users on delete cascade primary key,
  email text not null,
  full_name text,
  avatar_url text,
  stripe_customer_id text unique,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);

-- Create subscriptions table
create table public.user_subscriptions (
  id uuid default gen_random_uuid() primary key,
  user_id uuid references public.user_profiles(id) on delete cascade not null,
  stripe_subscription_id text unique not null,
  stripe_customer_id text not null,
  status text not null check (status in ('active', 'canceled', 'incomplete', 'past_due', 'unpaid', 'trialing')),
  price_id text not null,
  quantity integer not null default 1,
  current_period_start timestamp with time zone not null,
  current_period_end timestamp with time zone not null,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);

-- RLS policies
alter table public.user_profiles enable row level security;
alter table public.user_subscriptions enable row level security;

-- Users can only access their own data
create policy "Users can view own profile" on public.user_profiles
  for select using (auth.uid() = id);

create policy "Users can update own profile" on public.user_profiles
  for update using (auth.uid() = id);

create policy "Users can view own subscriptions" on public.user_subscriptions
  for select using (auth.uid() = user_id);

Generated TypeScript types (after running supabase gen types):

// types/database.types.ts
export interface Database {
  public: {
    Tables: {
      user_profiles: {
        Row: {
          id: string;
          email: string;
          full_name: string | null;
          avatar_url: string | null;
          stripe_customer_id: string | null;
          created_at: string;
          updated_at: string;
        };
        Insert: {
          id: string;
          email: string;
          full_name?: string | null;
          avatar_url?: string | null;
          stripe_customer_id?: string | null;
          created_at?: string;
          updated_at?: string;
        };
        Update: {
          id?: string;
          email?: string;
          full_name?: string | null;
          avatar_url?: string | null;
          stripe_customer_id?: string | null;
          updated_at?: string;
        };
      };
      user_subscriptions: {
        Row: {
          id: string;
          user_id: string;
          stripe_subscription_id: string;
          stripe_customer_id: string;
          status: 'active' | 'canceled' | 'incomplete' | 'past_due' | 'unpaid' | 'trialing';
          price_id: string;
          quantity: number;
          current_period_start: string;
          current_period_end: string;
          created_at: string;
          updated_at: string;
        };
        Insert: {
          id?: string;
          user_id: string;
          stripe_subscription_id: string;
          stripe_customer_id: string;
          status: 'active' | 'canceled' | 'incomplete' | 'past_due' | 'unpaid' | 'trialing';
          price_id: string;
          quantity?: number;
          current_period_start: string;
          current_period_end: string;
          created_at?: string;
          updated_at?: string;
        };
        Update: {
          id?: string;
          user_id?: string;
          stripe_subscription_id?: string;
          stripe_customer_id?: string;
          status?: 'active' | 'canceled' | 'incomplete' | 'past_due' | 'unpaid' | 'trialing';
          price_id?: string;
          quantity?: number;
          current_period_start?: string;
          current_period_end?: string;
          updated_at?: string;
        };
      };
    };
  };
}

Type-Safe User Registration Flow

Registration is where everything falls apart if you don't plan for failure. I've seen too many apps create a Supabase user, then fail to create the Stripe customer, leaving users in auth limbo. This registration service has saved my ass multiple times by cleaning up properly when things break.

The Supabase Admin API is solid for user creation, and Stripe's Customer creation is straightforward. You'll want to understand TypeScript error handling patterns and database transactions before you ship this.

Registration service that doesn't abandon users:

// lib/services/user-registration.service.ts
import { createServiceRoleClient } from '@/lib/supabase/service-role';
import { stripe } from '@/lib/stripe';
import { z } from 'zod';

const RegisterUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  fullName: z.string().min(1),
});

type RegisterUserInput = z.infer<typeof RegisterUserSchema>;

interface RegistrationResult {
  success: boolean;
  user?: {
    id: string;
    email: string;
    stripeCustomerId: string;
  };
  error?: string;
}

export async function registerUserWithStripeCustomer(
  input: RegisterUserInput
): Promise<RegistrationResult> {
  const supabase = createServiceRoleClient();
  
  try {
    // Validate input
    const { email, password, fullName } = RegisterUserSchema.parse(input);

    // Step 1: Create Supabase user
    const { data: authData, error: authError } = await supabase.auth.admin.createUser({
      email,
      password,
      email_confirm: true, // Skip email confirmation for demo
    });

    if (authError || !authData.user) {
      return {
        success: false,
        error: `Failed to create user: ${authError?.message}`,
      };
    }

    const userId = authData.user.id;

    try {
      // Step 2: Create Stripe customer
      const stripeCustomer = await stripe.customers.create({
        email,
        name: fullName,
        metadata: {
          supabase_user_id: userId,
        },
      });

      // Step 3: Create user profile with Stripe customer ID
      const { error: profileError } = await supabase
        .from('user_profiles')
        .insert({
          id: userId,
          email,
          full_name: fullName,
          stripe_customer_id: stripeCustomer.id,
        });

      if (profileError) {
        // Cleanup: Delete Stripe customer if profile creation fails
        await stripe.customers.del(stripeCustomer.id);
        throw new Error(`Profile creation failed: ${profileError.message}`);
      }

      return {
        success: true,
        user: {
          id: userId,
          email,
          stripeCustomerId: stripeCustomer.id,
        },
      };

    } catch (stripeError) {
      // Cleanup: Delete Supabase user if Stripe operations fail
      await supabase.auth.admin.deleteUser(userId);
      
      return {
        success: false,
        error: `Stripe integration failed: ${stripeError instanceof Error ? stripeError.message : 'Unknown error'}`,
      };
    }

  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Registration failed',
    };
  }
}

Subscription Management with Type Safety

Keeping subscription states in sync between Supabase and Stripe is a pain in the ass, but here's how I handle it without losing data when webhooks show up drunk and out of order.

Supabase realtime is cool for showing users instant updates, but don't trust it for critical state - I've seen webhooks arrive 10 minutes late and in random order. The Stripe Subscriptions API is solid once you understand the subscription lifecycle. Their webhook retry logic will save your ass when your server is down. Supabase realtime is great for UI updates, just don't bet the farm on it.

Subscription service that handles webhook chaos:

// lib/services/subscription.service.ts
import { createServiceRoleClient } from '@/lib/supabase/service-role';
import { stripe } from '@/lib/stripe';
import { Database } from '@/types/database.types';

type SubscriptionRow = Database['public']['Tables']['user_subscriptions']['Row'];
type SubscriptionInsert = Database['public']['Tables']['user_subscriptions']['Insert'];
type SubscriptionUpdate = Database['public']['Tables']['user_subscriptions']['Update'];

export class SubscriptionService {
  private supabase = createServiceRoleClient();

  /**
   * Create a new subscription for a user
   * This method ensures data consistency between Stripe and Supabase
   */
  async createSubscription(
    userId: string,
    priceId: string,
    options: {
      trialPeriodDays?: number;
      coupon?: string;
    } = {}
  ): Promise<SubscriptionRow | null> {
    try {
      // Get user profile with Stripe customer ID
      const { data: profile, error: profileError } = await this.supabase
        .from('user_profiles')
        .select('stripe_customer_id')
        .eq('id', userId)
        .single();

      if (profileError || !profile?.stripe_customer_id) {
        throw new Error('User profile or Stripe customer ID not found');
      }

      // Create subscription in Stripe
      const subscription = await stripe.subscriptions.create({
        customer: profile.stripe_customer_id,
        items: [{ price: priceId }],
        trial_period_days: options.trialPeriodDays,
        coupon: options.coupon,
        payment_behavior: 'default_incomplete',
        payment_settings: {
          save_default_payment_method: 'on_subscription',
        },
        expand: ['latest_invoice.payment_intent'],
      });

      // Store subscription in Supabase
      const subscriptionData: SubscriptionInsert = {
        user_id: userId,
        stripe_subscription_id: subscription.id,
        stripe_customer_id: profile.stripe_customer_id,
        status: subscription.status,
        price_id: priceId,
        quantity: subscription.items.data[0]?.quantity || 1,
        current_period_start: new Date(subscription.current_period_start * 1000).toISOString(),
        current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
      };

      const { data, error } = await this.supabase
        .from('user_subscriptions')
        .insert(subscriptionData)
        .select()
        .single();

      if (error) {
        // Cleanup: Cancel Stripe subscription if database insert fails
        await stripe.subscriptions.cancel(subscription.id);
        throw error;
      }

      return data;

    } catch (error) {
      console.error('Subscription creation failed:', error);
      return null;
    }
  }

  /**
   * Update subscription from Stripe webhook
   * Handles partial failures and data consistency
   */
  async syncSubscriptionFromWebhook(
    stripeSubscription: Stripe.Subscription
  ): Promise<boolean> {
    try {
      const subscriptionUpdate: SubscriptionUpdate = {
        status: stripeSubscription.status,
        price_id: stripeSubscription.items.data[0]?.price.id,
        quantity: stripeSubscription.items.data[0]?.quantity || 1,
        current_period_start: new Date(stripeSubscription.current_period_start * 1000).toISOString(),
        current_period_end: new Date(stripeSubscription.current_period_end * 1000).toISOString(),
        updated_at: new Date().toISOString(),
      };

      const { error } = await this.supabase
        .from('user_subscriptions')
        .update(subscriptionUpdate)
        .eq('stripe_subscription_id', stripeSubscription.id);

      if (error) {
        console.error('Subscription sync failed:', error);
        return false;
      }

      return true;
    } catch (error) {
      console.error('Webhook sync error:', error);
      return false;
    }
  }

  /**
   * Get user's active subscription with type safety
   */
  async getUserSubscription(userId: string): Promise<SubscriptionRow | null> {
    const { data, error } = await this.supabase
      .from('user_subscriptions')
      .select('*')
      .eq('user_id', userId)
      .eq('status', 'active')
      .order('created_at', { ascending: false })
      .limit(1)
      .single();

    if (error && error.code !== 'PGRST116') {
      console.error('Failed to fetch subscription:', error);
      return null;
    }

    return data;
  }

  /**
   * Check if user has active subscription
   * Useful for middleware and route protection
   */
  async hasActiveSubscription(userId: string): Promise<boolean> {
    const subscription = await this.getUserSubscription(userId);
    return subscription?.status === 'active';
  }
}

// Export singleton instance
export const subscriptionService = new SubscriptionService();

Auth Flow Diagram

Authentication Middleware with Subscription Checks

Middleware is where you'll spend way too much time optimizing database queries because every request hits this code. I've learned to be paranoid about performance here - one slow query and your app feels like it's running through molasses.

The Next.js middleware docs are actually decent, and Supabase auth helpers handle the heavy lifting. You'll want to understand Edge Runtime limitations and Supabase connection pooling before you deploy this and wonder why everything is slow.

Middleware that doesn't kill your performance:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { Database } from '@/types/database.types';

export async function middleware(request: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient<Database>({ req: request, res });

  // Refresh session if expired
  const {
    data: { session },
    error,
  } = await supabase.auth.getSession();

  // Public routes that don't require authentication
  const publicRoutes = ['/auth', '/pricing', '/'];
  const isPublicRoute = publicRoutes.some(route => 
    request.nextUrl.pathname.startsWith(route)
  );

  // Redirect unauthenticated users from protected routes
  if (!session && !isPublicRoute) {
    const redirectUrl = request.nextUrl.clone();
    redirectUrl.pathname = '/auth/login';
    redirectUrl.searchParams.set('redirectedFrom', request.nextUrl.pathname);
    return NextResponse.redirect(redirectUrl);
  }

  // Check subscription for premium routes
  const premiumRoutes = ['/dashboard', '/analytics', '/api/premium'];
  const isPremiumRoute = premiumRoutes.some(route =>
    request.nextUrl.pathname.startsWith(route)
  );

  if (session && isPremiumRoute) {
    // Check subscription status (with caching)
    const { data: subscription } = await supabase
      .from('user_subscriptions')
      .select('status')
      .eq('user_id', session.user.id)
      .eq('status', 'active')
      .single();

    if (!subscription) {
      const redirectUrl = request.nextUrl.clone();
      redirectUrl.pathname = '/pricing';
      redirectUrl.searchParams.set('upgrade', 'required');
      return NextResponse.redirect(redirectUrl);
    }
  }

  return res;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

This setup has kept my sanity intact through multiple production deployments. Your auth won't randomly break and your subscriptions will actually sync when they're supposed to.

TypeScript Integration Approaches Comparison

Approach

Type Safety Level

Development Speed

Runtime Safety

Maintenance Cost

Production Reliability

No TypeScript

❌ None

🟢 Fastest initial dev

❌ Runtime errors daily

🔴 High (debugging hell)

🔴 Poor ("undefined is not a function" crashes)

Basic TypeScript

🟡 Partial

🟡 Medium

🟡 Still debugging type coercion

🟡 Medium

🟡 Better but still breaks

Zod + Generated Types

🟢 High

🟡 Slower but worth it

🟢 Catches webhook chaos

🟢 Low (catches dumb mistakes)

🟢 Actually deployable

Full Type-Safe Stack

✅ Complete

🔴 Painful but pays off

✅ Bulletproof in prod

✅ Self-documenting magic

✅ Sleep better at night

Frequently Asked Questions

Q

Why do my types break every time I touch the database?

A

Because Supabase's type generation is finicky as hell. Your schema changes, you forget to regenerate types, then everything breaks at build time with "Property 'new_column' does not exist on type 'OldTableType'" errors. Set up this script, but expect it to randomly fail with connection timeouts. This tutorial says it takes 5 minutes - budget 2 hours for the bullshit edge cases:

#!/bin/bash
## scripts/sync-types.sh
npx supabase db diff --use-migra --file migrations/$(date +%Y%m%d_%H%M%S)_schema_changes.sql
npx supabase gen types typescript --project-id your-project-id > types/database.types.ts
npm run type-check

Run this after every schema change or enjoy "Type 'unknown' is not assignable to type 'string'" hell. Set up a pre-commit hook if you want to hate yourself less. When types are out of sync, your build fails with "TS2339: Property 'subscription_tier' does not exist" and you waste 20 minutes remembering you added that column yesterday.

Q

Why do my webhooks work locally then explode in production?

A

Because localhost is a lying bastard that never sends malformed data. Every webhook payload is perfect, every user input is sanitized, every network call succeeds. Then production happens. Everything works locally with your perfect test data, then prod throws Cannot read property 'id' of null when Stripe sends a webhook with missing fields. Add Zod validation or enjoy debugging during your vacation when customers start emailing about failed payments:

const WebhookEventSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('customer.subscription.updated'),
    data: z.object({
      object: SubscriptionSchema,
    }),
  }),
  z.object({
    type: z.literal('invoice.payment_succeeded'),
    data: z.object({
      object: InvoiceSchema,
    }),
  }),
]);

This catches Stripe's weird edge cases before they turn into "why didn't this customer get charged?" support tickets.

Q

How do I handle TypeScript errors with Next.js 15's async Request APIs?

A

Next.js 15 requires await for cookies(), headers(), and params(). Update your types and usage:

// Before (Next.js 14)
const cookieStore = cookies();
const session = cookieStore.get('session');

// After (Next.js 15)
const cookieStore = await cookies();
const session = cookieStore.get('session');

Use the Next.js codemod to automate most changes, but expect to fix edge cases manually.

Q

Should I use Prisma or Supabase's generated types for my TypeScript integration?

A

For Supabase projects, use Supabase's generated types. They're automatically synced with your database schema and work seamlessly with Supabase's JavaScript client. Prisma adds complexity without significant benefits in Supabase environments:

  • Supabase types: Direct schema mapping, automatic updates, smaller bundle size
  • Prisma: Better ORM features but requires additional configuration and database introspection

Only consider Prisma if you need advanced ORM features like complex queries or database-agnostic code.

Q

How do I type-safely handle Stripe webhook retries and duplicate events?

A

Store processed event IDs in your database with proper TypeScript validation:

interface ProcessedEvent {
  id: string;
  stripe_event_id: string;
  event_type: string;
  processed_at: string;
  processing_result: 'success' | 'failed';
}

async function handleWebhook(event: Stripe.Event) {
  // Check if already processed
  const { data: existing } = await supabase
    .from('processed_webhook_events')
    .select('id')
    .eq('stripe_event_id', event.id)
    .single();

  if (existing) {
    return { success: true, message: 'Already processed' };
  }

  // Process event and record result
  try {
    await processStripeEvent(event);
    
    await supabase
      .from('processed_webhook_events')
      .insert({
        stripe_event_id: event.id,
        event_type: event.type,
        processing_result: 'success',
      });

    return { success: true };
  } catch (error) {
    await supabase
      .from('processed_webhook_events')
      .insert({
        stripe_event_id: event.id,
        event_type: event.type,
        processing_result: 'failed',
      });
    
    throw error;
  }
}
Q

What's the best way to handle subscription status across Supabase and Stripe?

A

Maintain subscription state in Supabase as the source of truth, synchronized via webhooks. Create a service that handles state transitions:

type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'unpaid' | 'trialing';

class SubscriptionStateManager {
  async updateSubscriptionStatus(
    subscriptionId: string, 
    newStatus: SubscriptionStatus
  ) {
    const validTransitions = this.getValidTransitions();
    
    const { data: current } = await supabase
      .from('user_subscriptions')
      .select('status')
      .eq('stripe_subscription_id', subscriptionId)
      .single();

    if (!this.isValidTransition(current?.status, newStatus)) {
      throw new Error(`Invalid transition from ${current?.status} to ${newStatus}`);
    }

    return await supabase
      .from('user_subscriptions')
      .update({ status: newStatus, updated_at: new Date().toISOString() })
      .eq('stripe_subscription_id', subscriptionId);
  }
}
Q

How do I handle TypeScript strict mode errors in my integration?

A

Enable strict mode gradually to avoid overwhelming errors:

// tsconfig.json - gradual strict mode adoption
{
  "compilerOptions": {
    "strict": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true
  }
}

Address one strict check at a time, starting with the most critical payment-related code. Use type assertions sparingly and only when you're certain about the types.

Q

Does Zod make everything slow?

A

Validation adds maybe 2-3ms. Meanwhile your Stripe API calls take 200ms and Supabase queries take 50ms. Stop optimizing the wrong thing. I spent 3 hours optimizing Zod schemas to save 1ms, then realized my unoptimized database query was taking 800ms. The safety is worth the microscopic performance cost:

// Benchmark: Validating a subscription object
const subscriptionData = { /* large object */ };

console.time('zod-validation');
const result = SubscriptionSchema.parse(subscriptionData);
console.timeEnd('zod-validation'); // ~few ms

console.time('stripe-api-call');
await stripe.subscriptions.retrieve(subscriptionId);
console.timeEnd('stripe-api-call'); // ~forever
Q

How do I debug TypeScript errors in my Stripe integration?

A

Use TypeScript's compiler API to get detailed error information:

// Add to your development workflow
import { transpileModule } from 'typescript';

const result = transpileModule(sourceCode, {
  compilerOptions: {
    strict: true,
    target: 'ES2022',
  },
});

if (result.diagnostics?.length) {
  result.diagnostics.forEach(diagnostic => {
    console.log('TypeScript Error:', diagnostic.messageText);
  });
}

Also use VS Code's TypeScript error lens extension to see errors inline with your code.

Q

Should I use Server Actions or API Routes for TypeScript payment processing?

A

Use API Routes for payment processing. Server Actions are designed for form submissions, not complex payment flows:

// ✅ Good - API Route for payment processing
export async function POST(request: NextRequest) {
  const body = await request.json();
  const result = CreatePaymentIntentSchema.parse(body);
  
  const paymentIntent = await stripe.paymentIntents.create({
    amount: result.amount,
    currency: result.currency,
  });

  return NextResponse.json({ clientSecret: paymentIntent.client_secret });
}

// ❌ Avoid - Server Action for complex payment logic
export async function createPaymentIntent(formData: FormData) {
  // Server Actions don't return JSON responses easily
  // Type safety is more complex with FormData
}
Q

How do I test my TypeScript integration locally?

A

Set up a comprehensive local testing environment:

// tests/integration.test.ts
import { createClient } from '@supabase/supabase-js';
import Stripe from 'stripe';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

const stripe = new Stripe(process.env.STRIPE_TEST_KEY!);

describe('Stripe + Supabase Integration', () => {
  it('creates user with Stripe customer', async () => {
    // Test the full flow with type checking
    const user = await createUserWithStripeCustomer({
      email: 'test@example.com',
      password: 'password123',
      fullName: 'Test User',
    });

    expect(user.success).toBe(true);
    expect(user.user?.stripeCustomerId).toMatch(/^cus_/);
  });
});

Use Stripe's test mode and Supabase's local development environment for safe testing.

Q

What are the most common TypeScript integration mistakes to avoid?

A
  1. Not validating webhook payloads: Runtime errors from API changes
  2. Using any types: Defeats the purpose of TypeScript
  3. Ignoring null/undefined: Leads to runtime crashes
  4. Not handling async properly: Promises without await
  5. Mixing client and server types: Security vulnerabilities

Focus on these areas for the biggest impact on code reliability.

Essential TypeScript Integration Resources

Related Tools & Recommendations

compare
Recommended

Payment Processors Are Lying About AI - Here's What Actually Works in Production

After 3 Years of Payment Processor Hell, Here's What AI Features Don't Suck

Stripe
/compare/stripe/adyen/square/paypal/checkout-com/braintree/ai-automation-features-2025
100%
integration
Similar content

Vite React 19 TypeScript ESLint 9: Production Setup Guide

Skip the 30-second Webpack wait times - This setup boots in about a second

Vite
/integration/vite-react-typescript-eslint/integration-overview
98%
integration
Recommended

Supabase + Next.js + Stripe: How to Actually Make This Work

The least broken way to handle auth and payments (until it isn't)

Supabase
/integration/supabase-nextjs-stripe-authentication/customer-auth-payment-flow
97%
pricing
Recommended

Our Database Bill Went From $2,300 to $980

competes with Supabase

Supabase
/pricing/supabase-firebase-planetscale-comparison/cost-optimization-strategies
78%
tool
Similar content

SvelteKit Performance Optimization: Fix Slow Apps & Boost Speed

Users are bailing because your site loads like shit on mobile - here's what actually works

SvelteKit
/tool/sveltekit/performance-optimization
75%
review
Recommended

Vite vs Webpack vs Turbopack: Which One Doesn't Suck?

I tested all three on 6 different projects so you don't have to suffer through webpack config hell

Vite
/review/vite-webpack-turbopack/performance-benchmark-review
73%
integration
Recommended

Stop Your APIs From Breaking Every Time You Touch The Database

Prisma + tRPC + TypeScript: No More "It Works In Dev" Surprises

Prisma
/integration/prisma-trpc-typescript/full-stack-architecture
58%
integration
Recommended

Stripe Terminal React Native Production Integration Guide

Don't Let Beta Software Ruin Your Weekend: A Reality Check for Card Reader Integration

Stripe Terminal
/integration/stripe-terminal-react-native/production-deployment-guide
55%
integration
Recommended

Bun + React + TypeScript + Drizzle Stack Setup Guide

Real-world integration experience - what actually works and what doesn't

Bun
/integration/bun-react-typescript-drizzle/performance-stack-overview
54%
compare
Recommended

I Tested Every Heroku Alternative So You Don't Have To

Vercel, Railway, Render, and Fly.io - Which one won't bankrupt you?

Vercel
/compare/vercel/railway/render/fly/deployment-platforms-comparison
50%
integration
Recommended

Claude API Code Execution Integration - Advanced Tools Guide

Build production-ready applications with Claude's code execution and file processing tools

Claude API
/integration/claude-api-nodejs-express/advanced-tools-integration
47%
review
Recommended

Which JavaScript Runtime Won't Make You Hate Your Life

Two years of runtime fuckery later, here's the truth nobody tells you

Bun
/review/bun-nodejs-deno-comparison/production-readiness-assessment
47%
integration
Recommended

Claude API + Express.js - Production Integration Guide

Stop fucking around with tutorials that don't work in production

Claude API
/integration/claude-api-nodejs-express/complete-implementation-guide
47%
tool
Similar content

Checkout.com: Enterprise Payments for High-Volume Businesses

Built for enterprise scale - when Stripe and PayPal aren't enough

Checkout.com
/tool/checkout-com/enterprise-payment-powerhouse
46%
integration
Similar content

Build a Payment Orchestration Layer: Stop Multi-Processor SDK Hell

Build a Payment Orchestration Layer That Actually Works in Production

Primer
/integration/multi-payment-processor-setup/orchestration-layer-setup
46%
alternatives
Recommended

Firebase Alternatives That Don't Suck - Real Options for 2025

Your Firebase bills are killing your budget. Here are the alternatives that actually work.

Firebase
/alternatives/firebase/best-firebase-alternatives
44%
integration
Recommended

How to Build Flutter Apps with Firebase Without Losing Your Sanity

Real-world production deployment that actually works (and won't bankrupt you)

Firebase
/integration/firebase-flutter/production-deployment-architecture
44%
integration
Recommended

Claude API + Next.js App Router: What Actually Works in Production

I've been fighting with Claude API and Next.js App Router for 8 months. Here's what actually works, what breaks spectacularly, and how to avoid the gotchas that

Claude API
/integration/claude-api-nextjs-app-router/app-router-integration
44%
tool
Recommended

Next.js App Router - File-System Based Routing for React

App Router breaks everything you know about Next.js routing

Next.js App Router
/tool/nextjs-app-router/overview
44%
compare
Recommended

Supabase vs Firebase vs Appwrite vs PocketBase - Which Backend Won't Fuck You Over

I've Debugged All Four at 3am - Here's What You Need to Know

Supabase
/compare/supabase/firebase/appwrite/pocketbase/backend-service-comparison
42%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization