Why This Stack Sucks Less Than Everything Else

I've tried custom auth with JWT tokens - spent 3 months and ended up with more security holes than Swiss cheese. I've wrestled with NextAuth.js configs that randomly break when you update anything. I built payment processing from scratch once and learned why you don't fucking do that.

Supabase + Next.js + Stripe still sucks sometimes, but it's the first combo that didn't make me want to quit programming and become a farmer.

The Parts That Actually Work

Supabase Auth: Less Painful Than Rolling Your Own

Supabase

Supabase Auth handles user registration, login, and all the tedious password reset stuff. The real win is Row Level Security (RLS) - your database automatically filters data by user without you having to remember to add WHERE user_id = ? to every goddamn query.

The JWT tokens contain user info that works across your Next.js app and Stripe. When auth works, you get:

  • Sessions that don't randomly expire (mostly)
  • User data that stays in sync
  • Database queries that can't leak other users' data
  • Social logins that actually work without fighting OAuth

But tokens die after exactly 1 hour like clockwork, and if your refresh logic is broken, users get kicked to the login screen right in the middle of using your app. Discovered this delightful feature during a client demo while 8 board members watched our "seamless user experience" crash and burn.

Next.js App Router: Server Components Are Tricky

Next.js

The Next.js App Router wants everything server-side, which is great until you realize auth state is complicated. The `@supabase/ssr` package tries to solve this, but you need two different Supabase clients.

One for server (reads cookies), one for client (handles real-time updates). Mix them up and you'll debug for 4 hours trying to figure out why users show up as authenticated on page load but immediately get booted. Error message? "TypeError: Cannot read property 'user' of undefined". Thanks, Next.js 14, super helpful.

Stripe: The Part That Usually Works

Stripe

Stripe's API is solid, but connecting it to Supabase users is where things get interesting. You need to:

Create Stripe customers after someone signs up. Simple, right? Wrong. Hit a race condition where users would sign up but never get a Stripe customer ID. Failed a ton - maybe 30%? Could've been more, hard to track because the error logging was also broken. Spent a whole Saturday debugging this until I found a Stack Overflow comment buried at the bottom: the auth.users trigger fires before the profiles table insert finishes. Async hell.

Handle subscriptions through Stripe's billing system. The subscription data lives in Stripe, but you need to sync status to your database. Webhooks do this, but they arrive out of order and sometimes fail. Build idempotency or suffer.

Process payments with Stripe Checkout. This part actually works reliably - Stripe handles the credit card headaches, you just redirect users and wait for webhook confirmations. The Stripe Elements integration offers more customization if needed, while Payment Intents API gives you full control over the payment flow.

What Performance Actually Looks Like

Here's what you can realistically expect:

Security is decent - RLS policies protect your database, Stripe handles PCI compliance, and you don't store credit card data. But you're still responsible for proper session management and webhook signature verification. Mess up either one and you're in trouble.

Performance is a shitshow. Auth is usually fast, except when it isn't. Supabase can be slow as molasses if you're far from their data centers. Stripe is reliable until you hit traffic spikes, then it can take 2+ seconds to respond. Cold starts are the real killer - first page load after deploy can take forever. Sometimes 2+ seconds. Users think your site died.

Scaling works up to maybe 50k users before you need to think harder. After that, you'll hit database connection limits, webhook processing bottlenecks, and realize Supabase's connection pooling isn't magic. You might need PgBouncer or Prisma connection pooling for better performance. Database optimization becomes critical at scale.

The Data Sync Nightmare You Signed Up For

Data Flow Diagram

You now have data scattered across three systems that need to stay in sync:

  1. Supabase: User profiles, app data, subscription status
  2. Stripe: Payment methods, billing history, actual money stuff
  3. Your Next.js app: Whatever cached state you're trying to keep consistent

Stripe webhooks are supposed to keep everything in sync. When payments succeed or fail, Stripe tells your webhook endpoint, which updates Supabase. Simple, right?

Nope. Webhooks show up out of order, fail without telling you, and sometimes arrive 6 hours late for no fucking reason. Had a customer pay for a subscription, webhook didn't show up for like 6 hours or something. They couldn't access the app and support was getting angry emails. Could've been more, hard to track because the error logging was also broken at the time.

"Eventually consistent" means "your data is fucked but maybe it'll fix itself later".

Security: Better Than Rolling Your Own

The security model is actually pretty solid when you don't screw it up. Understanding PostgreSQL RLS and JWT token validation is essential:

RLS policies in Supabase automatically filter queries by user ID from the JWT token. No more forgetting WHERE user_id = ? and accidentally leaking someone else's data. Just don't use the service role key in client code - that bypasses RLS and exposes everything.

Next.js middleware runs on every request to check auth status. It's supposed to refresh expired tokens automatically, but sometimes it doesn't and users get bounced to the login page mid-session. Fun times.

Stripe webhook signatures prevent people from faking payment events. Always verify these or someone will post fake "payment succeeded" events to your webhook and get free subscriptions. I've seen it happen.

Why Not Build It Yourself?

I've tried the DIY route. Here's why I don't anymore:

  • Time: This stack took me 1-2 weeks to get working well vs 2-3 months building auth and payments from scratch
  • Security: Supabase and Stripe handle PCI compliance and SOC 2 requirements I don't want to think about. DIY means expensive audits and sleepless nights
  • Maintenance: Vendor updates happen automatically. With custom code, every security patch is your problem
  • Features: Getting 2FA, social logins, and subscription management working properly takes forever

Yeah, you're locked into these vendors. But the alternative is maintaining authentication and payment infrastructure forever. Pick your poison.

The Gotchas That'll Bite You

Webhook Flow

Here's what breaks in production:

Webhook chaos: Stripe sends webhooks out of order and retries them when they fail. Build idempotency handling or watch your subscription statuses flip-flop randomly. I spent a whole weekend debugging this after launch.

Auth state mysteries: Server components render with one user state, client hydrates with another. Users see flickers, get logged out randomly, or worse - see other people's data for a split second. Loading states are your friend.

Database connections: Every API route opens database connections. Hit any decent traffic and you'll exhaust Supabase's connection pool. Enable connection pooling or your app dies under load.

Token expiration: Tokens expire after an hour. If your refresh logic sucks, users get bounced to login mid-session. Always happens during the most important user flows.

Building This Mess (Buckle Up)

Development Setup

This will take 3x longer than you estimate. I told the client "one week", delivered in three, and that was with working nights. Don't be an idiot like me - pad your estimates.

Phase 1: Dependencies and Environment Hell

Start With Next.js (And Pray)

Use Next.js 14 - latest stable version works best:

npx create-next-app@latest saas-app --typescript --tailwind --app
cd saas-app
npm install @supabase/supabase-js @supabase/ssr stripe

Gotcha: Node 18.2.0-18.4.0 has some OpenSSL bug that makes create-next-app shit the bed with "crypto" errors that make no sense. Use 18.17+ or you'll waste 2 hours Googling error messages.

Environment Variables (The Fun Part)

Create .env.local and prepare for pain:

## Supabase - get these from your dashboard
NEXT_PUBLIC_SUPABASE_URL=https://abcdefg.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6...

## Stripe - test keys first, obviously  
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

## Your app URL - gets you every time
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Critical shit to remember:

  • The NEXT_PUBLIC_ prefix is required for client-side access
  • Service role key bypasses all security - keep it server-side only
  • Wrong SITE_URL breaks OAuth redirects in mysterious ways
  • First deployment: CORS blocked everything because I put https://yourapp.com/ instead of https://yourapp.com. Spent 3 hours debugging CORS errors before noticing the trailing slash. Wanted to punch my monitor.

Phase 2: Database Schema (Where Things Start Breaking)

Database Schema

Go to your Supabase SQL Editor and run this. Copy-paste exactly - don't get creative. I tried to "optimize" this once and broke everything for a week:

-- Extend auth.users with profile data
CREATE TABLE public.profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  full_name TEXT,
  avatar_url TEXT,
  stripe_customer_id TEXT UNIQUE,
  subscription_status TEXT DEFAULT 'inactive',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
  PRIMARY KEY (id)
);

-- Enable RLS on profiles table
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Users can only see and update their own profile
CREATE POLICY "Users can view own profile" ON public.profiles
  FOR SELECT USING (auth.uid() = id);

CREATE POLICY "Users can update own profile" ON public.profiles  
  FOR UPDATE USING (auth.uid() = id);

-- Create profile automatically when user signs up
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
  INSERT INTO public.profiles (id, full_name, avatar_url)
  VALUES (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
  RETURN new;
END;
$$ language plpgsql security definer;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

What the fuck just happened: You made a profiles table that auto-creates when users sign up. The trigger is async, which causes that race condition I mentioned earlier where Stripe customers fail to create.

RLS gotcha: These policies only work when auth.uid() returns something. If you're testing with direct SQL queries, they'll return empty results and you'll think everything is broken.

Phase 3: Auth Implementation (Where Dreams Die)

Auth Flow

The Two-Client Mindfuck

You need separate clients for server and browser. Mix them up and debug for 3 hours wondering why auth works on page load but dies on interaction. Learned this the hard way:

Browser Client (lib/supabase/client.ts):

import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Server Client (lib/supabase/server.ts):

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) => {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Server components can't write cookies - middleware handles refresh
          }
        },
      },
    }
  )
}

Server client gotcha: The try/catch is there because server components can't write cookies - they just explode with cryptic errors instead. Took me a day to figure this out from a random GitHub comment.

Middleware: The Black Magic Part

Create middleware.ts in your project root to handle authentication state. Next.js middleware runs on the Edge Runtime with specific limitations:

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value }) => 
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  // Protect dashboard routes
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone()
    url.pathname = '/auth/signin'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

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

Middleware bullshit:

  • Runs on every request, even API routes. Mess up the matcher and you get infinite redirect loops.
  • Edge runtime times out after 30 seconds but nobody fucking documents this. When Supabase is having issues, auth just stops working with zero error messages.
  • Cookie handling is brittle as hell. Touch it wrong and everything breaks.

Phase 4: Stripe Integration (Race Condition Hell)

Customer Creation With Retry Logic

Create lib/actions/stripe.ts and prepare for the race condition I mentioned earlier:

'use server'

import Stripe from 'stripe'
import { createClient } from '@/lib/supabase/server'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-08-27', // Use latest stable version - check Stripe docs for current
})

export async function createStripeCustomer(userId: string, email: string, name?: string) {
  const supabase = await createClient()
  
  // Wait for profile to exist (race condition fix)
  let profile = null
  let attempts = 0
  while (!profile && attempts < 5) {
    const { data } = await supabase
      .from('profiles')
      .select('stripe_customer_id')
      .eq('id', userId)
      .single()
    
    if (data) {
      profile = data
      break
    }
    
    // Profile doesn't exist yet, wait and try again
    await new Promise(resolve => setTimeout(resolve, 1000))
    attempts++
  }

  if (!profile) {
    throw new Error('Profile not found after retries - something went wrong')
  }

  if (profile.stripe_customer_id) {
    return profile.stripe_customer_id
  }

  // Create Stripe customer
  const customer = await stripe.customers.create({
    email,
    name,
    metadata: {
      supabase_user_id: userId,
    },
  })

  // Update profile with Stripe customer ID
  await supabase
    .from('profiles')
    .update({ stripe_customer_id: customer.id })
    .eq('id', userId)

  return customer.id
}

Subscription Management

Handle subscription creation and management:

export async function createCheckoutSession(customerId: string, priceId: string) {
  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    payment_method_types: ['card'],
    line_items: [
      {
        price: priceId,
        quantity: 1,
      },
    ],
    mode: 'subscription',
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
    metadata: {
      user_id: customerId,
    },
  })

  return session.url
}

export async function createCustomerPortalSession(customerId: string) {
  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard`,
  })

  return session.url
}

Phase 5: Webhook Handler (The Most Important Part)

Create app/api/webhooks/stripe/route.ts. Get this wrong and your subscription states will be fucked forever:

import { headers } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { createClient } from '@/lib/supabase/server'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-08-27', // Use latest stable version - check Stripe docs for current
})

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!

export async function POST(req: NextRequest) {
  const body = await req.text()
  const signature = headers().get('stripe-signature')!
  
  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
  } catch (err) {
    console.error('Webhook signature verification failed:', err)
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  const supabase = await createClient()

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated':
      const subscription = event.data.object as Stripe.Subscription
      await supabase
        .from('profiles')
        .update({
          subscription_status: subscription.status,
          updated_at: new Date().toISOString(),
        })
        .eq('stripe_customer_id', subscription.customer as string)
      break

    case 'customer.subscription.deleted':
      await supabase
        .from('profiles')
        .update({
          subscription_status: 'canceled',
          updated_at: new Date().toISOString(),
        })
        .eq('stripe_customer_id', event.data.object.customer as string)
      break

    case 'invoice.payment_failed':
      const invoice = event.data.object as Stripe.Invoice
      await supabase
        .from('profiles')
        .update({
          subscription_status: 'past_due',
          updated_at: new Date().toISOString(),
        })
        .eq('stripe_customer_id', invoice.customer as string)
      break
  }

  return NextResponse.json({ received: true })
}

Critical webhook bullshit:

  • Use await req.text(), not req.json() or signature verification fails
  • Always return { received: true } within 30 seconds or Stripe retries forever. I once had a webhook that took way too long - maybe 45 seconds? - and got hit with hundreds of duplicate payments.
  • Handle duplicate events - Stripe sends the same webhook multiple times
  • Check the webhook endpoint URL in Stripe dashboard matches exactly: /api/webhooks/stripe

Here's what saved my ass: Use Stripe CLI during development: stripe listen --forward-to localhost:3000/api/webhooks/stripe

Phase 6: Frontend Components (Almost Done)

Protected Dashboard Component

Create app/dashboard/page.tsx:

import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { createStripeCustomer, createCustomerPortalSession } from '@/lib/actions/stripe'

export default async function DashboardPage() {
  const supabase = await createClient()
  
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) redirect('/auth/signin')

  const { data: profile } = await supabase
    .from('profiles')
    .select('*')
    .eq('id', user.id)
    .single()

  // Create Stripe customer if doesn't exist
  if (!profile?.stripe_customer_id) {
    await createStripeCustomer(user.id, user.email!, profile?.full_name)
  }

  async function openCustomerPortal() {
    'use server'
    if (profile?.stripe_customer_id) {
      const portalUrl = await createCustomerPortalSession(profile.stripe_customer_id)
      redirect(portalUrl!)
    }
  }

  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
      
      <div className="bg-white rounded-lg shadow p-6 mb-6">
        <h2 className="text-xl font-semibold mb-4">Account Information</h2>
        <p className="mb-2"><strong>Email:</strong> {user.email}</p>
        <p className="mb-4"><strong>Status:</strong> {profile?.subscription_status || 'inactive'}</p>
        
        <form action={openCustomerPortal}>
          <button 
            type="submit"
            className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          >
            Manage Billing
          </button>
        </form>
      </div>
    </div>
  )
}

Dashboard gotchas:

  • Always check for user existence before trying to access profile data
  • The createStripeCustomer call can take 8-12 seconds due to retry logic. Users will think your app is broken. Add a loading spinner.
  • Server actions in forms can cause hydration mismatches if not handled carefully

Phase 7: Testing (Where You Find All The Bugs)

Testing Locally (Prepare for Pain)

Use Stripe CLI to forward webhooks. Install it via npm or Homebrew. The webhook testing guide covers local development:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

Testing checklist:

  • User signup creates profile (check the database)
  • Stripe customer gets created (with retry logic)
  • Webhooks actually update subscription status
  • Auth state persists across browser refresh
  • User can't access other users' data (test this!)

Use test card numbers:

  • 4242424242424242 for successful payments
  • 4000000000000002 for declined cards
  • 4000000000003220 for 3D Secure authentication

Production (Good Luck)

Webhook setup: Add your production webhook URL in Stripe Dashboard. Use the live webhook secret, not test. Check webhook best practices for security and the webhook events reference for handling different event types.

Environment variables: Triple-check these in production. Wrong SITE_URL will break social auth.

Database: Run the same SQL schema in production. Supabase CLI helps with database migrations but double-check everything. Use pg_dump for complex migrations.

Monitoring: Set up logging for webhook failures. When (not if) payments break, you need to know immediately.

Reality check: Production will be different. Something always breaks when you deploy for real, and it's never the thing you expect.

What Actually Works vs What Breaks Your Spirit

Architecture Pattern

Reality Check

Time to Working

Things That Break

Why You'll Hate It

Supabase + Next.js

Actually works

1-2 weeks of pain

Token refresh hell, webhook chaos

Vendor lock-in anxiety

NextAuth.js + Database

Overrated garbage

2-3 weeks of confusion

Every update breaks config

Community toxic as fuck

Custom JWT + API Routes

Career suicide

3+ months of misery

You'll get hacked

Should have used Supabase

Firebase Auth

Google will kill it

1-2 weeks

Firestore costs $$$$

RIP Google Reader

Questions From Developers Who Survived This Hell

Q

Why the hell is Stripe customer creation failing with "user not found"?

A

Because you're trying to create a Stripe customer right after user signup, but Supabase's profile creation trigger is async as fuck. Race condition hits randomly and you'll want to scream.

Q

Webhooks failing with "signature verification failed" - what's fucked now?

A

This error shows up constantly. Here's what you screwed up:

  1. Wrong endpoint: Must be EXACTLY /api/webhooks/stripe. Not /webhooks/stripe, not /api/stripe/webhooks. Stripe is picky as hell.
  2. Body parsing fail: You used await req.json() instead of await req.text(). Signature verification needs raw text or it shits the bed.
  3. Wrong webhook secret: Test vs production keys. Mix these up and nothing works but you get no useful error message.
  4. Platform bullshit: Some hosts modify request bodies. Vercel is fine, others... not so much.
Q

Server components say user is not authenticated but middleware should be handling this

A

Middleware runs on Edge Runtime, server components run on Node.js runtime. Different cookie access, different auth state. It's a shitshow.

Stop going insane:

const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
  redirect('/auth/signin')
}
Q

Users keep getting logged out and I'm losing my shit

A

Welcome to auth hell. These are the usual suspects:

  1. Token expiration: Tokens die after 1 hour like clockwork. If your refresh logic is broken, users get kicked mid-session and complain to support.
  2. Clock drift: Your server clock is different from Supabase's. Tokens look expired when they're not, so validation randomly fails.
  3. Middleware timeouts: Edge Runtime times out after 30 seconds but this isn't documented anywhere. Auth just stops working and you'll blame everything except the timeout.

Stop the madness: Add proper token refresh and never show users raw auth errors. They don't give a shit about JWT validation failures.

Q

Server says user is logged in, client says logged out, I'm going insane

A

Welcome to the server/client hydration nightmare. Server components render at request time, client components hydrate at render time. If tokens refresh between these moments, you get different auth states.

How to keep your sanity: Never assume auth state matches between server and client. Always show loading states and handle auth mismatches gracefully.

Q

Social login redirects to wrong URL in production and stakeholders are pissed

A

Your Supabase settings are wrong:

  1. Site URL: Must match your domain EXACTLY. https://yourapp.com, not https://yourapp.com/ or http://yourapp.com.
  2. Redirect URLs: Add your auth callback: https://yourapp.com/auth/callback
  3. OAuth providers: Google/GitHub settings must have matching redirect URIs or they'll reject the auth flow.
Q

Stripe checkout works locally but completely shits the bed in production

A

Of course it does. Here's what's probably fucked:

  1. Webhook endpoint unreachable: Your prod server is behind some firewall or proxy that blocks Stripe's requests.
  2. Missing HTTPS: Stripe requires HTTPS in production. No HTTP, no exceptions.
  3. CORS bullshit: If you're using custom domains, CORS might be blocking Stripe's domains.
  4. Platform rate limiting: Your hosting provider is rate-limiting webhook endpoints because they hate you.
Q

Customer portal shows wrong data or just breaks completely

A

Your webhook synchronization is broken:

  1. Customer ID not syncing: Webhook handlers aren't updating stripe_customer_id properly.
  2. Database permissions fucked: You're not using the service role key for webhook operations.
  3. Duplicate events: You're not handling webhook idempotency, so duplicate events corrupt your data.
Q

Subscription status in my database doesn't match Stripe and users are complaining

A

Webhook processing is failing somewhere:

  1. Check webhook logs: Stripe is probably sending events that you're not handling or are failing.
  2. Missing event types: You're only handling subscription.created but ignoring subscription.updated.
  3. Failed webhook processing: Database operations are failing and you're not retrying.
  4. Nuclear option: Use Stripe CLI to replay events: stripe events resend evt_1234567890
Q

RLS policies are blocking queries that should work and I'm about to lose it

A

RLS is powerful but fucking confusing. Common mistakes:

  1. Missing auth.uid(): Your policies need auth.uid() = user_id comparisons. No auth context = no data.
  2. Wrong policy type: Use FOR ALL for most cases, or be specific with FOR SELECT, FOR UPDATE.
  3. Service role confusion: Service role key bypasses RLS completely. Use it in webhooks, not user queries.

Debug this clusterfuck: Run EXPLAIN (ANALYZE, BUFFERS) SELECT ... to see which policies are blocking your queries.

Q

Auth is slower than molasses (500ms+ response times)

A

Your auth is slow as shit because:

  1. Connection pool exhausted: Every request creates database connections. Enable Supabase connection pooling or die.
  2. Complex RLS policies: Your policies are doing table scans. Add indexes or simplify the logic.
  3. Cold start hell: Edge Runtime cold starts add 100-300ms. Nothing you can do about it.
  4. Repeated auth checks: Stop fetching user data on every request. Cache that shit.
Q

Real-time subscriptions randomly die after auth changes

A

Supabase real-time connections use JWT tokens. When tokens refresh, existing connections become invalid and stop receiving updates.

Fix the chaos:

useEffect(() => {
  const channel = supabase.channel('changes')
    .on('postgres_changes', ...)
    .subscribe()

  return () => supabase.removeChannel(channel)
}, [user]) // Re-subscribe when user changes
Q

Environment variables are undefined and everything is broken

A

Your deployment config is fucked:

  1. Missing NEXT_PUBLIC_ prefix: Client-side variables need this prefix or they're undefined in the browser.
  2. Build vs runtime confusion: Some platforms need env vars at build time, others at runtime. Check your platform docs.
  3. Case sensitivity: STRIPE_SECRET_KEYstripe_secret_key. Production is pickier than your laptop.
  4. Platform-specific bullshit: Each deployment platform handles env vars differently. Vercel ≠ Netlify ≠ Railway.
Q

CORS errors calling Supabase and I want to throw my laptop

A

Supabase should handle CORS automatically, but you probably fucked up the config:

  1. Wrong Site URL: Must match your domain exactly. https://yourapp.com, not https://yourapp.com/dashboard.
  2. www vs non-www: Both need to be configured or one will be blocked.
  3. Local development: Set Site URL to http://localhost:3000 for local dev.
  4. Multiple environments: Use separate Supabase projects for dev/staging/prod or cross-contamination will ruin your day.
Q

Webhooks timeout and Stripe starts retrying like crazy

A

Webhook reliability is shit because:

  1. 30-second timeout: Stripe gives you 30 seconds max. Take longer and they retry forever.
  2. Database timeouts: Connection pool exhausted during traffic spikes.
  3. Cold start delays: Serverless functions can take 2+ seconds to start up.
  4. No retry logic: Database operations fail and you're not handling it gracefully.
Q

Is storing Stripe customer IDs in Supabase secure or am I asking to get hacked?

A

It's fine if you don't fuck it up:

  1. RLS policies work: Users can only see their own customer ID, not everyone else's.
  2. Customer IDs aren't sensitive: Stripe designed them to be stored in your database.
  3. No payment data: You're storing references, not credit card numbers.
  4. Don't use service role key client-side: That bypasses all security and exposes everything.
Q

Can users hack their subscription status by modifying the database directly?

A

Not if you set up RLS correctly:

  1. Webhook-only updates: Only your webhook endpoint should modify subscription status.
  2. Service role key for webhooks: Webhooks bypass RLS because they need admin access.
  3. User policies are read-only: Users can SELECT their subscription status, not UPDATE it.
Q

What happens when (not if) Supabase or Stripe goes down?

A

Both have good uptime but shit happens:

  1. Cache auth sessions: Don't rely on real-time auth checks for everything.
  2. Stripe queues webhooks: Failed webhook deliveries get retried automatically.
  3. Graceful degradation: Show cached data when services are down.
  4. Monitor status pages: Set up alerts for service outages.
Q

How to test webhooks locally without wanting to die

A

Use Stripe CLI to forward webhooks:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

Copy the webhook signing secret it gives you and use it in your .env.local. This part usually works fine.

Q

Should I run Supabase locally or use the cloud?

A

Use cloud for development - it's easier. Local Supabase setup can be tricky with Docker.

Go local only if:

  • You need to test complex database migrations
  • You're working offline
  • Your company has air-gapped development requirements
Q

Testing without creating a million fake accounts

A

Create test users with the admin API:

const { data, error } = await supabaseAdmin.auth.admin.createUser({
  email: 'test@example.com',
  password: 'test123456',
  email_confirm: true
})

For payments, use Stripe's test cards:

  • 4242424242424242 - Success
  • 4000000000000002 - Declined
  • 4000000000003220 - 3D Secure required

Bottom line: production is always different. Something will work locally but not in prod, guaranteed.

Resources That Don't Completely Suck

Related Tools & Recommendations

compare
Similar content

Stripe vs Adyen vs Square AI: Real Payment Processing Features

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%
compare
Recommended

Framework Wars Survivor Guide: Next.js, Nuxt, SvelteKit, Remix vs Gatsby

18 months in Gatsby hell, 6 months testing everything else - here's what actually works for enterprise teams

Next.js
/compare/nextjs/nuxt/sveltekit/remix/gatsby/enterprise-team-scaling
85%
integration
Similar content

Stripe React Native Firebase: Complete Auth & Payment Flow Guide

Stripe + React Native + Firebase: A Guide to Not Losing Your Mind

Stripe
/integration/stripe-react-native-firebase/complete-authentication-payment-flow
62%
compare
Similar content

Stripe, Plaid, Dwolla, Yodlee: Unbiased Fintech API Comparison

Comparing: Stripe | Plaid | Dwolla | Yodlee

Stripe
/compare/stripe/plaid/dwolla/yodlee/payment-ecosystem-showdown
58%
compare
Similar content

Stripe, Adyen, Square, PayPal, Checkout.com: Processor Battle

Five payment processors that each break in spectacular ways when you need them most

Stripe
/compare/stripe/adyen/square/paypal/checkout-com/payment-processor-battle
55%
integration
Similar content

Supabase Next.js 13+ Server-Side Auth Guide: What Works & Fixes

Here's what actually works (and what will break your app)

Supabase
/integration/supabase-nextjs/server-side-auth-guide
51%
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
43%
tool
Similar content

Stripe Overview: Payment Processing & API Ecosystem Guide

Finally, a payment platform that won't make you want to throw your laptop out the window when debugging webhooks at 3am

Stripe
/tool/stripe/overview
43%
integration
Similar content

Stripe Plaid Integration: KYC & Identity Verification to Stop Fraud

KYC setup that catches fraud single vendors miss

Stripe
/integration/stripe-plaid/identity-verification-kyc
38%
compare
Similar content

Stripe vs Plaid vs Dwolla - The 3AM Production Reality Check

Comparing a race car, a telescope, and a forklift - which one moves money?

Stripe
/compare/stripe/plaid/dwolla/production-reality-check
37%
tool
Similar content

Next.js Overview: Features, Benefits & Next.js 15 Updates

Explore Next.js, the powerful React framework with built-in routing, SSR, and API endpoints. Understand its core benefits, when to use it, and what's new in Nex

Next.js
/tool/nextjs/overview
36%
tool
Similar content

SvelteKit: Fast Web Apps & Why It Outperforms Alternatives

I'm tired of explaining to clients why their React checkout takes 5 seconds to load

SvelteKit
/tool/sveltekit/overview
34%
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
34%
compare
Recommended

Remix vs SvelteKit vs Next.js: Which One Breaks Less

I got paged at 3AM by apps built with all three of these. Here's which one made me want to quit programming.

Remix
/compare/remix/sveltekit/ssr-performance-showdown
32%
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
32%
pricing
Recommended

What Enterprise Platform Pricing Actually Looks Like When the Sales Gloves Come Off

Vercel, Netlify, and Cloudflare Pages: The Real Costs Behind the Marketing Bullshit

Vercel
/pricing/vercel-netlify-cloudflare-enterprise-comparison/enterprise-cost-analysis
32%
pricing
Recommended

Got Hit With a $3k Vercel Bill Last Month: Real Platform Costs

These platforms will fuck your budget when you least expect it

Vercel
/pricing/vercel-vs-netlify-vs-cloudflare-pages/complete-pricing-breakdown
32%
compare
Similar content

Astro, Next.js, Gatsby: Static Site Generator Benchmark

Just use fucking Astro. Next.js if you actually need server shit. Gatsby is dead - seriously, stop asking.

Astro
/compare/astro/nextjs/gatsby/static-generation-performance-benchmark
31%
tool
Similar content

Astro Overview: Static Sites, React Integration & Astro 5.0

Explore Astro, the static site generator that solves JavaScript bloat. Learn about its benefits, React integration, and the game-changing content features in As

Astro
/tool/astro/overview
30%
tool
Recommended

Nuxt - I Got Tired of Vue Setup Hell

Vue framework that does the tedious config shit for you, supposedly

Nuxt
/tool/nuxt/overview
30%

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