Create a new Next.js project with TypeScript, Tailwind CSS, and the App Router. This is the foundation of your app.
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:
cd my-app
npm run dev
Open http://localhost:3000, you should see the Next.js welcome page.
- src/
- app/
- layout.tsx - Root layout
- page.tsx - Home page
- globals.css - Global styles
- app/
- tailwind.config.ts
- next.config.js
- package.json
"scripts": { "dev": "next dev --turbo" } to your package.json for faster development builds with Turbopack.
Supabase gives you a Postgres database, authentication, and realtime subscriptions. All in one.
Create a Supabase project
- Go to supabase.com/dashboard
- Click "New Project"
- Choose a name and strong database password (save this!)
- Select a region close to your users
- Wait ~2 minutes for provisioning
Install the Supabase packages
npm install @supabase/supabase-js @supabase/ssr
Add environment variables
Get your project URL and anon key from Supabase Dashboard → Settings → API.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Create the Supabase client
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
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
}
},
},
}
)
}
service_role key in client-side code. The anon key is safe for browsers.
Supabase Auth supports email/password, magic links, and OAuth providers. We'll set up email authentication.
Create a middleware for auth
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
'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>
)
}
Stripe handles payments, subscriptions, and invoicing. We'll set up a basic checkout flow.
Install Stripe
npm install stripe @stripe/stripe-js
Add your Stripe keys
Get your keys from Stripe Dashboard → Developers → API keys.
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Create a checkout API route
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
'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>
)
}
Resend is a modern email API. Great for transactional emails like welcome messages and receipts.
npm install resend
Get your API key from resend.com/api-keys.
RESEND_API_KEY=re_...
Create an email API route
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 })
}
}
onboarding@resend.dev for testing.
Vercel makes deployment trivial. Push to GitHub, connect to Vercel, done.
Push to GitHub
git init
git add .
git commit -m "Initial commit"
gh repo create my-app --public --source=. --push
Connect to Vercel
- Go to vercel.com/new
- Import your GitHub repository
- Add your environment variables (copy from .env.local)
- Click Deploy
Your app will be live at your-app.vercel.app in about 60 seconds.
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