Back to Building Stack
🔨 Complete Setup Guide

Build your first app with the Building Stack

A step-by-step guide to setting up Next.js, Supabase, Vercel, Stripe, and Resend. From zero to deployed in under an hour.

Before you start

1

Next.js Set up Next.js

~5 minutes

Create a new Next.js project with TypeScript, Tailwind CSS, and the App Router. This is the foundation of your app.

Terminal
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

When prompted, accept the defaults. Then navigate into your project:

Terminal
cd my-app
npm run dev

Open http://localhost:3000, you should see the Next.js welcome page.

Your project structure:
  • src/
    • app/
      • layout.tsx - Root layout
      • page.tsx - Home page
      • globals.css - Global styles
  • tailwind.config.ts
  • next.config.js
  • package.json
💡 Tip: Add "scripts": { "dev": "next dev --turbo" } to your package.json for faster development builds with Turbopack.
2

Supabase Configure Supabase

~10 minutes

Supabase gives you a Postgres database, authentication, and realtime subscriptions. All in one.

Create a Supabase project

  1. Go to supabase.com/dashboard
  2. Click "New Project"
  3. Choose a name and strong database password (save this!)
  4. Select a region close to your users
  5. Wait ~2 minutes for provisioning

Install the Supabase packages

Terminal
npm install @supabase/supabase-js @supabase/ssr

Add environment variables

Get your project URL and anon key from Supabase Dashboard → Settings → API.

.env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Create the Supabase client

src/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!
  )
}
src/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() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Handle server component context
          }
        },
      },
    }
  )
}
⚠️ Important: Never expose your service_role key in client-side code. The anon key is safe for browsers.
3

Supabase Implement Authentication

~15 minutes

Supabase Auth supports email/password, magic links, and OAuth providers. We'll set up email authentication.

Create a middleware for auth

src/middleware.ts
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() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => 
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // Refresh session if expired
  await supabase.auth.getUser()

  return supabaseResponse
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Create a simple login page

src/app/login/page.tsx
'use client'

import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })

    if (error) {
      alert(error.message)
    } else {
      router.push('/dashboard')
      router.refresh()
    }
    setLoading(false)
  }

  const handleSignUp = async () => {
    setLoading(true)
    const { error } = await supabase.auth.signUp({
      email,
      password,
    })

    if (error) {
      alert(error.message)
    } else {
      alert('Check your email for the confirmation link!')
    }
    setLoading(false)
  }

  return (
    <div className="min-h-screen flex items-center justify-center">
      <form onSubmit={handleLogin} className="w-full max-w-sm space-y-4">
        <h1 className="text-2xl font-bold">Login</h1>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="w-full p-3 border rounded"
          required
        />
        <input
          type="password"
          placeholder="Password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          className="w-full p-3 border rounded"
          required
        />
        <button
          type="submit"
          disabled={loading}
          className="w-full p-3 bg-black text-white rounded"
        >
          {loading ? 'Loading...' : 'Login'}
        </button>
        <button
          type="button"
          onClick={handleSignUp}
          disabled={loading}
          className="w-full p-3 border rounded"
        >
          Create Account
        </button>
      </form>
    </div>
  )
}
💡 Tip: Enable email confirmations in Supabase Dashboard → Authentication → Settings for production apps.
4

Stripe Add Stripe Payments

~10 minutes

Stripe handles payments, subscriptions, and invoicing. We'll set up a basic checkout flow.

Install Stripe

Terminal
npm install stripe @stripe/stripe-js

Add your Stripe keys

Get your keys from Stripe Dashboard → Developers → API keys.

.env.local (add to existing)
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

Create a checkout API route

src/app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'

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

export async function POST(request: Request) {
  const { priceId } = await request.json()

  try {
    const session = await stripe.checkout.sessions.create({
      mode: 'subscription', // or 'payment' for one-time
      payment_method_types: ['card'],
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: `${request.headers.get('origin')}/success`,
      cancel_url: `${request.headers.get('origin')}/pricing`,
    })

    return NextResponse.json({ url: session.url })
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}

Create a pricing button

src/components/CheckoutButton.tsx
'use client'

export function CheckoutButton({ priceId }: { priceId: string }) {
  const handleCheckout = async () => {
    const res = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ priceId }),
    })
    const { url } = await res.json()
    window.location.href = url
  }

  return (
    <button onClick={handleCheckout} className="bg-black text-white px-6 py-3 rounded">
      Subscribe
    </button>
  )
}
💡 Tip: Create your products and prices in Stripe Dashboard → Products before using priceId.
5

Resend Set up Resend for Emails

~5 minutes

Resend is a modern email API. Great for transactional emails like welcome messages and receipts.

Terminal
npm install resend

Get your API key from resend.com/api-keys.

.env.local (add to existing)
RESEND_API_KEY=re_...

Create an email API route

src/app/api/email/route.ts
import { Resend } from 'resend'
import { NextResponse } from 'next/server'

const resend = new Resend(process.env.RESEND_API_KEY)

export async function POST(request: Request) {
  const { to, subject, html } = await request.json()

  try {
    const data = await resend.emails.send({
      from: 'Your App <hello@yourdomain.com>',
      to,
      subject,
      html,
    })

    return NextResponse.json(data)
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}
⚠️ Note: You'll need to verify a domain in Resend to send from a custom address. Use onboarding@resend.dev for testing.
6

Vercel Deploy to Vercel

~5 minutes

Vercel makes deployment trivial. Push to GitHub, connect to Vercel, done.

Push to GitHub

Terminal
git init
git add .
git commit -m "Initial commit"
gh repo create my-app --public --source=. --push

Connect to Vercel

  1. Go to vercel.com/new
  2. Import your GitHub repository
  3. Add your environment variables (copy from .env.local)
  4. Click Deploy

Your app will be live at your-app.vercel.app in about 60 seconds.

💡 Tip: Enable "Automatically expose System Environment Variables" in Vercel project settings for cleaner config.

Final checklist

  • All environment variables added to Vercel
  • Supabase URL added to allowed redirect URLs (Auth settings)
  • Stripe webhook endpoint configured for production
  • Resend domain verified for production emails

Put this stack into action

Follow a workflow to get from tools to results.

🤖 AI-Powered Development Use AI tools to code faster and ship smarter. 💡 From Idea to MVP in a Weekend Validate, build, and ship your first version in 48 hours.