Vercel, Supabase & the Modern Developer Ecosystem

Building a full-stack application used to mean weeks of setup — provisioning servers, configuring databases, setting up authentication, wiring email services, managing deployments. In 2026, a new generation of developer platforms has changed the game.
Vercel deploys your frontend with a git push. Supabase gives you a PostgreSQL database with authentication, real-time subscriptions, and storage — all in minutes. Around them, an ecosystem of specialized tools handles everything else: auth, email, payments, caching, and more.
This guide explores the modern developer ecosystem — what each tool does, how they work together, and when you should (or shouldn't) use this stack.
What You'll Learn
✅ What Vercel is and how it deploys frontend apps at the edge
✅ What Supabase is and how it replaces Firebase with PostgreSQL
✅ The ecosystem of tools: Clerk, Drizzle, Upstash, Resend, Stripe, and more
✅ How to architect a full-stack app with the modern stack
✅ When to use this stack vs traditional cloud (AWS/GCP/Azure)
✅ Cost analysis and free tier capabilities
✅ Best practices for production deployments
Part 1: The Modern Developer Platform Landscape
What Changed?
Traditional cloud providers (AWS, GCP, Azure) are powerful but complex. Deploying a simple web app on AWS involves:
- Setting up EC2 or ECS for compute
- Configuring RDS for the database
- Setting up CloudFront for CDN
- Managing IAM roles, VPCs, security groups
- Setting up CI/CD pipelines
- Configuring SSL certificates
- Monitoring with CloudWatch
The modern developer ecosystem takes a different approach: each tool does one thing extremely well, with minimal configuration and generous free tiers.
The Key Principles
| Principle | Traditional Cloud | Modern Ecosystem |
|---|---|---|
| Setup time | Hours to days | Minutes |
| Configuration | Manual / IaC | Convention over configuration |
| Scaling | Manual / auto-scaling policies | Automatic, serverless |
| Pricing | Pay for resources (running 24/7) | Pay per usage (scale to zero) |
| Deployment | CI/CD pipelines, Docker, K8s | git push |
| Database | Provision and manage yourself | Managed with built-in branching |
| Free tier | Limited (12-month trials) | Generous (often enough for side projects) |
Part 2: Vercel — The Frontend Cloud
What is Vercel?
Vercel is a frontend cloud platform built by the creators of Next.js. It deploys web applications to a global edge network with zero configuration.
At its core, Vercel is:
- A deployment platform (push code → get a URL)
- An edge network (your app runs close to users worldwide)
- A serverless runtime (API routes run as functions)
- A developer experience layer (preview deployments, analytics, speed insights)
How Vercel Works
When you push to GitHub:
- Vercel automatically detects the framework (Next.js, Vite, Nuxt, Remix, etc.)
- Runs the build command
- Deploys static assets to the global CDN
- Deploys API routes / server components as serverless functions
- Gives you a unique preview URL for each branch/commit
Key Vercel Features
Preview Deployments
Every pull request gets its own deployment URL. This means:
- Designers can review UI changes before merge
- QA can test features on real infrastructure
- Stakeholders can see progress without running code locally
# Every PR automatically gets a URL like:
# https://my-app-git-feature-login-yourteam.vercel.appServerless Functions
API routes in Next.js automatically become serverless functions on Vercel:
// app/api/users/route.ts — Next.js App Router
import { NextResponse } from 'next/server';
export async function GET() {
const users = await db.query('SELECT * FROM users');
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.insert('users', body);
return NextResponse.json(user, { status: 201 });
}Edge Middleware
Run code before the request reaches your application. Useful for authentication, redirects, A/B testing, and geolocation:
// middleware.ts — runs at the edge (fast, global)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add geolocation headers
const country = request.geo?.country || 'US';
const response = NextResponse.next();
response.headers.set('x-user-country', country);
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};Incremental Static Regeneration (ISR)
Serve static pages that automatically update in the background:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}Analytics & Speed Insights
Built-in performance monitoring:
- Web Vitals tracking (LCP, FID, CLS, TTFB)
- Real user monitoring (not synthetic benchmarks)
- Per-page performance breakdown
- Speed Insights with actionable recommendations
Vercel Pricing
| Feature | Hobby (Free) | Pro ($20/mo) | Enterprise |
|---|---|---|---|
| Deployments | Unlimited | Unlimited | Unlimited |
| Bandwidth | 100 GB | 1 TB | Custom |
| Serverless Execution | 100 GB-hours | 1000 GB-hours | Custom |
| Preview Deployments | ✅ | ✅ | ✅ |
| Analytics | Basic | Advanced | Advanced |
| Team Members | 1 | Unlimited | Unlimited |
| Commercial Use | ❌ | ✅ | ✅ |
| Password Protection | ❌ | ✅ | ✅ |
Tip: The Hobby plan is generous enough for personal projects and side projects. You need Pro for commercial applications or team collaboration.
Vercel vs Alternatives
| Feature | Vercel | Netlify | Cloudflare Pages |
|---|---|---|---|
| Best For | Next.js apps | Static sites, JAMstack | Edge-first apps |
| Framework Support | All (Next.js is first-class) | All | All |
| Edge Functions | ✅ Edge Middleware | ✅ Edge Functions | ✅ Workers |
| Serverless Functions | ✅ (Node.js, Go, Python) | ✅ (Node.js) | ✅ (Workers) |
| Build Speed | Fast | Fast | Fast |
| Free Bandwidth | 100 GB | 100 GB | Unlimited |
| ISR Support | Native | Via plugins | Via Workers |
| Analytics | Built-in | Via plugin | Built-in |
| Price (Pro) | $20/mo | $19/mo | $5/mo (Workers Paid) |
Bottom line: Choose Vercel for Next.js apps (best integration). Choose Netlify for simpler static sites. Choose Cloudflare Pages for the cheapest option with unlimited bandwidth.
Part 3: Supabase — The Open Source Firebase Alternative
What is Supabase?
Supabase is an open-source backend-as-a-service (BaaS) built on top of PostgreSQL. It provides the features developers typically need from Firebase — but with a real relational database instead of a document store.
Core Features
1. PostgreSQL Database
Supabase gives you a full PostgreSQL database — not a proprietary NoSQL store. This means:
- Standard SQL queries
- Joins, transactions, constraints
- Extensions (PostGIS, pgvector, pg_cron)
- Full compatibility with any PostgreSQL tool
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Query data
const { data: posts, error } = await supabase
.from('posts')
.select('*, author:profiles(name, avatar_url)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10);
// Insert data
const { data, error: insertError } = await supabase
.from('posts')
.insert({
title: 'My New Post',
content: 'Hello world!',
author_id: user.id,
})
.select()
.single();2. Auto-Generated REST & GraphQL APIs
Supabase automatically generates RESTful APIs from your database schema using PostgREST. Every table, view, and function gets an API endpoint — with no code to write.
3. Authentication
Built-in authentication with 20+ providers:
// Email/password sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword123',
});
// OAuth (Google, GitHub, Discord, etc.)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
},
});
// Magic link (passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
});
// Get current user
const { data: { user } } = await supabase.auth.getUser();4. Row Level Security (RLS)
RLS is the killer feature. It lets you define who can access which rows directly in the database — no backend middleware needed:
-- Enable RLS on posts table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Anyone can read published posts
CREATE POLICY "Public can read published posts"
ON posts FOR SELECT
USING (published = true);
-- Users can only insert their own posts
CREATE POLICY "Users can insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Users can only update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Users can only delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);With RLS, your client-side code can query the database directly — the security rules are enforced at the database level:
// Client-side: this is SAFE because RLS is enforced
// User will only see published posts (or their own)
const { data: posts } = await supabase
.from('posts')
.select('*');5. Realtime Subscriptions
Listen to database changes in real-time:
// Subscribe to new posts
const channel = supabase
.channel('public:posts')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
console.log('New post:', payload.new);
}
)
.subscribe();
// Presence (who's online)
const presenceChannel = supabase.channel('room-1');
presenceChannel
.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState();
console.log('Online users:', Object.keys(state).length);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await presenceChannel.track({ user_id: user.id, name: user.name });
}
});6. Edge Functions
Serverless functions written in TypeScript/Deno that run close to your users:
// supabase/functions/send-welcome-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
const { email, name } = await req.json();
// Send email via Resend
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'welcome@yourapp.com',
to: email,
subject: `Welcome, ${name}!`,
html: `<h1>Welcome to our app, ${name}!</h1>`,
}),
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
});7. Storage
File storage with access control tied to your database policies:
// Upload a file
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${user.id}/profile.png`, file, {
cacheControl: '3600',
upsert: true,
});
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(`${user.id}/profile.png`);Supabase vs Alternatives
| Feature | Supabase | Firebase | Neon | PlanetScale |
|---|---|---|---|---|
| Database | PostgreSQL | Firestore (NoSQL) | PostgreSQL | MySQL |
| Open Source | ✅ | ❌ | ❌ | ❌ |
| SQL Support | Full SQL | No SQL | Full SQL | Full SQL |
| Auth | Built-in | Built-in | ❌ (use third-party) | ❌ (use third-party) |
| Realtime | Built-in | Built-in | ❌ | ❌ |
| Edge Functions | ✅ (Deno) | ✅ (Node.js) | ❌ | ❌ |
| Storage | Built-in | Built-in | ❌ | ❌ |
| RLS | ✅ | Security Rules | ✅ (manual) | ❌ |
| Branching | ✅ | ❌ | ✅ | ✅ |
| Self-Host | ✅ | ❌ | ❌ | ❌ |
| Free Tier DB | 500 MB | 1 GB | 512 MB | 5 GB |
Bottom line: Choose Supabase for a complete backend with auth and realtime. Choose Firebase if you prefer NoSQL and Google ecosystem. Choose Neon or PlanetScale if you only need a managed database (no auth/storage).
Supabase Pricing
| Feature | Free | Pro ($25/mo) | Team ($599/mo) |
|---|---|---|---|
| Database | 500 MB | 8 GB | 16 GB |
| Bandwidth | 5 GB | 250 GB | 500 GB |
| Storage | 1 GB | 100 GB | 200 GB |
| Edge Functions | 500K invocations | 2M invocations | 5M invocations |
| Auth MAUs | 50,000 | 100,000 | 100,000+ |
| Realtime | 200 concurrent | 500 concurrent | Custom |
| Support | Community | Priority |
Part 4: The Ecosystem — Tools That Work Together
The modern stack isn't just Vercel + Supabase. It's a whole ecosystem of specialized tools that integrate seamlessly.
Authentication: Clerk & Auth.js
While Supabase has built-in auth, sometimes you need more advanced features:
Clerk
Clerk provides drop-in authentication UI components with advanced features:
// app/layout.tsx — Clerk with Next.js
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html>
<body>{children}</body>
</html>
</ClerkProvider>
);
}// app/dashboard/page.tsx — protected route
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
return <h1>Welcome to your dashboard</h1>;
}Clerk is great when you need:
- Pre-built sign-in/sign-up UI components
- Multi-factor authentication (MFA)
- Organization/team management
- Session management dashboard
- User impersonation for support
Auth.js (NextAuth)
Auth.js is the open-source alternative for Next.js authentication:
// auth.ts — Auth.js configuration
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! }),
Google({ clientId: process.env.GOOGLE_ID!, clientSecret: process.env.GOOGLE_SECRET! }),
],
});| Feature | Supabase Auth | Clerk | Auth.js |
|---|---|---|---|
| Price | Free (50K MAUs) | Free (10K MAUs) | Free (open source) |
| UI Components | Basic | Beautiful, pre-built | Build your own |
| MFA | ✅ | ✅ | Via providers |
| Organizations | ❌ | ✅ | ❌ |
| Self-hosted | ✅ | ❌ | ✅ |
| Best For | Supabase projects | Polished auth UX | Custom auth flows |
Database ORMs: Drizzle & Prisma
When working with Supabase's PostgreSQL (or Neon), you'll want a type-safe ORM:
Drizzle ORM
Drizzle is a lightweight, type-safe ORM that generates zero overhead:
// db/schema.ts — Drizzle schema
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false),
authorId: text('author_id').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});// Query with full type safety
import { db } from './db';
import { posts } from './db/schema';
import { eq, desc } from 'drizzle-orm';
const publishedPosts = await db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);
// TypeScript knows the exact shape of publishedPostsPrisma
Prisma uses a schema-first approach with its own SDL:
// prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
authorId String
createdAt DateTime @default(now())
author User @relation(fields: [authorId], references: [id])
}// Query with Prisma
const publishedPosts = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
include: { author: true },
});| Feature | Drizzle | Prisma |
|---|---|---|
| Approach | Code-first (TypeScript) | Schema-first (SDL) |
| Bundle Size | ~50 KB | ~800 KB |
| SQL-like Syntax | ✅ | ❌ (own API) |
| Migrations | SQL migrations | Prisma Migrate |
| Raw SQL | Easy | $queryRaw |
| Edge Support | ✅ | ✅ (with adapters) |
| Learning Curve | Low (if you know SQL) | Low (intuitive API) |
| Best For | SQL-savvy devs | Teams wanting abstraction |
Caching & Queues: Upstash
Upstash provides serverless Redis and Kafka — pay per request, scale to zero:
// Rate limiting with Upstash
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
});
// In your API route
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return new Response('Too many requests', { status: 429 });
}
// Process request...
}// Caching with Upstash Redis
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export async function getPost(slug: string) {
// Check cache first
const cached = await redis.get<Post>(`post:${slug}`);
if (cached) return cached;
// Fetch from database
const post = await db.query.posts.findFirst({
where: eq(posts.slug, slug),
});
// Cache for 1 hour
await redis.set(`post:${slug}`, post, { ex: 3600 });
return post;
}Upstash also provides:
- QStash — serverless message queue (delayed tasks, webhooks)
- Vector — serverless vector database (for AI/RAG applications)
- Workflow — durable serverless workflows
Email: Resend & React Email
Resend is a developer-first email API. React Email lets you build emails with React components:
// emails/welcome.tsx — React Email template
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
export function WelcomeEmail({ name, loginUrl }: { name: string; loginUrl: string }) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif', background: '#f4f4f5' }}>
<Container style={{ maxWidth: '480px', margin: '0 auto', padding: '20px' }}>
<Text style={{ fontSize: '24px', fontWeight: 'bold' }}>
Welcome, {name}!
</Text>
<Text>Thanks for signing up. Click below to get started:</Text>
<Button
href={loginUrl}
style={{ background: '#000', color: '#fff', padding: '12px 24px', borderRadius: '6px' }}
>
Go to Dashboard
</Button>
</Container>
</Body>
</Html>
);
}// Send email with Resend
import { Resend } from 'resend';
import { WelcomeEmail } from '@/emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
await resend.emails.send({
from: 'App <hello@yourapp.com>',
to: user.email,
subject: 'Welcome to Our App!',
react: WelcomeEmail({ name: user.name, loginUrl: 'https://yourapp.com/dashboard' }),
});Payments: Stripe
Stripe remains the standard for payments. With Next.js and the modern stack:
// app/api/checkout/route.ts — Create Stripe checkout session
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
});
return Response.json({ url: session.url });
}// app/api/webhooks/stripe/route.ts — Handle Stripe webhooks
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
// Activate user's subscription in your database
await activateSubscription(session.customer as string);
break;
case 'customer.subscription.deleted':
const subscription = event.data.object;
// Deactivate subscription
await deactivateSubscription(subscription.customer as string);
break;
}
return Response.json({ received: true });
}Other Notable Tools
| Tool | Category | What It Does | Free Tier |
|---|---|---|---|
| Neon | Database | Serverless PostgreSQL with branching | 512 MB |
| Turso | Database | SQLite at the edge (libSQL) | 9 GB storage |
| Lemon Squeezy | Payments | Stripe alternative (handles tax/billing) | Free (fees per transaction) |
| Inngest | Background Jobs | Event-driven serverless functions | 25K events/mo |
| Trigger.dev | Background Jobs | Open-source serverless background jobs | 50K runs/mo |
| PostHog | Analytics | Product analytics, feature flags, session replay | 1M events/mo |
| Sentry | Monitoring | Error tracking, performance monitoring | 5K errors/mo |
Part 5: Building a Full-Stack App with the Modern Stack
Let's build a practical architecture using these tools together. We'll design a SaaS application with authentication, a database, payments, and email.
Architecture Overview
Project Setup
# Create Next.js project
npx create-next-app@latest my-saas-app --typescript --tailwind --app --src-dir
cd my-saas-app
# Install ecosystem packages
npm install @supabase/supabase-js @clerk/nextjs @upstash/redis @upstash/ratelimit resend stripe
npm install drizzle-orm postgres
npm install -D drizzle-kitEnvironment Variables
# .env.local
# Vercel (auto-populated when deployed)
NEXT_PUBLIC_URL=http://localhost:3000
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
# Upstash
UPSTASH_REDIS_REST_URL=https://xxxxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=AXxx...
# Resend
RESEND_API_KEY=re_...
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...Database Schema with Drizzle
// src/db/schema.ts
import { pgTable, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
export const profiles = pgTable('profiles', {
id: text('id').primaryKey(), // Clerk user ID
email: text('email').notNull().unique(),
name: text('name'),
avatarUrl: text('avatar_url'),
stripeCustomerId: text('stripe_customer_id'),
plan: text('plan').default('free'), // 'free' | 'pro' | 'team'
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const projects = pgTable('projects', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull(),
description: text('description'),
ownerId: text('owner_id').notNull().references(() => profiles.id),
isPublic: boolean('is_public').default(false),
createdAt: timestamp('created_at').defaultNow(),
});
export const apiKeys = pgTable('api_keys', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
projectId: text('project_id').notNull().references(() => projects.id),
name: text('name').notNull(),
key: text('key').notNull().unique(),
lastUsedAt: timestamp('last_used_at'),
requestCount: integer('request_count').default(0),
createdAt: timestamp('created_at').defaultNow(),
});Authentication Middleware
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/pricing',
'/blog(.*)',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)',
]);
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect();
}
});
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
};API Route with Rate Limiting
// src/app/api/projects/route.ts
import { auth } from '@clerk/nextjs/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { db } from '@/db';
import { projects } from '@/db/schema';
import { eq } from 'drizzle-orm';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(20, '60 s'),
});
export async function GET() {
const { userId } = await auth();
if (!userId) return new Response('Unauthorized', { status: 401 });
// Rate limit
const { success } = await ratelimit.limit(userId);
if (!success) return new Response('Too many requests', { status: 429 });
const userProjects = await db
.select()
.from(projects)
.where(eq(projects.ownerId, userId));
return Response.json(userProjects);
}
export async function POST(request: Request) {
const { userId } = await auth();
if (!userId) return new Response('Unauthorized', { status: 401 });
const { name, description } = await request.json();
const [project] = await db
.insert(projects)
.values({ name, description, ownerId: userId })
.returning();
return Response.json(project, { status: 201 });
}Deploying to Vercel
# Install Vercel CLI
npm install -g vercel
# Link to Vercel project
vercel link
# Add environment variables
vercel env add SUPABASE_SERVICE_ROLE_KEY
vercel env add CLERK_SECRET_KEY
vercel env add STRIPE_SECRET_KEY
# ... (or add via Vercel Dashboard → Settings → Environment Variables)
# Deploy
vercel --prod
# Or just push to GitHub — Vercel auto-deploysPart 6: When to Use This Stack vs Traditional Cloud
The Decision Framework
Choose the Modern Stack When
✅ Building an MVP or side project — ship in days, not weeks
✅ You're a solo developer or small team — less infrastructure to manage
✅ Your app is a web application (SaaS, dashboard, marketplace, blog)
✅ You want generous free tiers — build without spending money
✅ You prioritize developer experience — less boilerplate, more building
✅ You need preview environments — test every PR on real infrastructure
Choose Traditional Cloud (AWS/GCP/Azure) When
✅ You need custom networking (VPCs, VPNs, private subnets)
✅ You're running ML workloads, GPU instances, or heavy compute
✅ You need strict compliance (HIPAA, FedRAMP, data residency)
✅ You have a dedicated DevOps/platform team
✅ You need Kafka, Elasticsearch, or specialized services at scale
✅ You're processing massive data volumes (data pipelines, ETL)
Limitations of the Modern Stack
| Limitation | Details | Mitigation |
|---|---|---|
| Vendor lock-in | Vercel-specific features (Edge Middleware, ISR) | Keep core logic framework-agnostic |
| Cold starts | Serverless functions have startup latency (50-500ms) | Use edge functions, keep functions warm |
| Egress costs | Bandwidth can get expensive at scale | Cache aggressively, optimize payloads |
| Scaling ceiling | Serverless has concurrency limits | Contact sales for higher limits |
| Limited compute | No long-running processes, no GPU | Use traditional cloud for heavy compute |
| Database size | Free tiers are small (500MB-5GB) | Upgrade to paid plans as needed |
Migration Paths
If you outgrow the modern stack, migration is straightforward:
Vercel → Self-hosted Next.js:
# Next.js works anywhere Node.js runs
npm run build
npm run start # Standard Node.js server
# Or use Docker
docker build -t my-app .
docker run -p 3000:3000 my-appSupabase → Self-hosted PostgreSQL:
- Supabase is open-source — you can self-host the entire stack
- Or just take your PostgreSQL database and connect directly
- Drizzle/Prisma schemas work with any PostgreSQL instance
Clerk → Auth.js or custom auth:
- Export user data via Clerk API
- Implement Auth.js with the same OAuth providers
- Migrate sessions during a transition period
Part 7: Cost Analysis
Free Tier Stack (MVP / Side Project)
Building a complete app with $0/month:
| Tool | Free Tier | What You Get |
|---|---|---|
| Vercel | Hobby | Unlimited deployments, 100 GB bandwidth |
| Supabase | Free | 500 MB database, 50K auth users, 1 GB storage |
| Clerk | Free | 10,000 monthly active users |
| Upstash | Free | 10K commands/day Redis, 500 messages/day QStash |
| Resend | Free | 3,000 emails/month |
| Stripe | Free | Pay only per-transaction fees (2.9% + $0.30) |
| Sentry | Free | 5,000 errors/month |
| PostHog | Free | 1M events/month |
| Total | $0/month | Enough for thousands of users |
Growth Stack ($50-100/month)
When you outgrow free tiers:
| Tool | Plan | Cost |
|---|---|---|
| Vercel | Pro | $20/month |
| Supabase | Pro | $25/month |
| Clerk | Pro | $25/month (10K+ MAUs) |
| Upstash | Pay-as-you-go | ~$5-10/month |
| Resend | Pro | $20/month |
| Total | ~$95-100/month |
This stack can handle tens of thousands of active users with excellent performance.
Comparison with Traditional Cloud
| Modern Stack | AWS Equivalent | |
|---|---|---|
| Compute | Vercel ($20) | EC2 / ECS (~$30-100) |
| Database | Supabase ($25) | RDS (~$30-60) |
| Auth | Clerk ($25) | Cognito (~$10-50) + dev time |
| CDN | Included in Vercel | CloudFront (~$10-30) |
| Redis | Upstash ($5) | ElastiCache (~$15-50) |
| Resend ($20) | SES (~$5) + dev time | |
| Monitoring | PostHog/Sentry (free) | CloudWatch (~$10-30) |
| DevOps time | Minimal | Significant |
| Total | ~$95/month | ~$110-320/month + DevOps salary |
The modern stack often costs less and requires significantly less DevOps expertise.
Part 8: Best Practices
1. Environment Management
# Use Vercel's environment variable system
# Separate values for Production, Preview, and Development
# .env.local → Local development only (never committed)
# Vercel Dashboard → Production environment variables
# Vercel Dashboard → Preview environment variables (for PR deployments)Rule: Never commit secrets to Git. Use .env.local for development and Vercel's dashboard for production.
2. Database Branching
Both Supabase and Neon support database branching — create isolated database copies for each PR:
# Supabase branching (via CLI)
supabase db branch create feature-new-schema
# Neon branching (via CLI or API)
neonctl branches create --name feature-new-schemaThis means each preview deployment on Vercel can have its own database with test data.
3. Type Safety Across the Stack
Use TypeScript everywhere and share types:
// types/database.ts — generated from your schema
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
import { profiles, projects } from '@/db/schema';
export type Profile = InferSelectModel<typeof profiles>;
export type NewProfile = InferInsertModel<typeof profiles>;
export type Project = InferSelectModel<typeof projects>;
export type NewProject = InferInsertModel<typeof projects>;4. Error Handling Pattern
// lib/safe-action.ts — type-safe server actions
export async function safeAction<T>(
fn: () => Promise<T>
): Promise<{ data: T; error: null } | { data: null; error: string }> {
try {
const data = await fn();
return { data, error: null };
} catch (err) {
console.error(err);
return { data: null, error: err instanceof Error ? err.message : 'Unknown error' };
}
}5. Monitoring & Observability
// Instrument your app with Sentry
// next.config.mjs
import { withSentryConfig } from '@sentry/nextjs';
export default withSentryConfig(nextConfig, {
org: 'your-org',
project: 'your-project',
silent: true,
});6. Security Checklist
- ✅ Enable RLS on all Supabase tables
- ✅ Use
SUPABASE_SERVICE_ROLE_KEYonly in server-side code - ✅ Validate webhook signatures (Stripe, Clerk)
- ✅ Rate limit all API endpoints
- ✅ Set proper CORS headers
- ✅ Use
httpOnlycookies for auth tokens - ✅ Never expose secret keys in client-side code
Conclusion
The modern developer ecosystem has fundamentally changed how we build web applications. Instead of spending weeks on infrastructure, you can have a production-ready full-stack app deployed in hours.
Here's the final summary:
| Component | Recommended Tool | Why |
|---|---|---|
| Deployment | Vercel | Best Next.js integration, preview deployments |
| Database | Supabase | PostgreSQL + auth + realtime in one package |
| Auth | Clerk or Supabase Auth | Clerk for advanced UX, Supabase Auth for simplicity |
| ORM | Drizzle | Lightweight, type-safe, SQL-like |
| Caching | Upstash Redis | Serverless, pay-per-request |
| Resend | Developer-first API, React Email templates | |
| Payments | Stripe | Industry standard, excellent docs |
| Analytics | PostHog | Open-source, generous free tier |
| Monitoring | Sentry | Error tracking, performance monitoring |
The practical truth: This stack is ideal for MVPs, startups, side projects, and small-to-medium SaaS applications. It lets you focus on building your product instead of managing infrastructure. When you outgrow it, the migration paths are clear — and by then, you'll have the revenue to justify the investment.
Start with free tiers. Ship fast. Scale when you need to.
Further Reading
- TypeScript Phase 4: Full-Stack Integration — Monorepo architecture with Next.js
- Bun vs Node.js vs Deno — Choose your JavaScript runtime
- What is REST API? Complete Guide — REST API design patterns
- HTTP Protocol Complete Guide — Understand HTTP for web development
- Database Schema Design Guide — Design your database properly
- Spring Boot OAuth2 & Social Login — OAuth2 concepts and implementation
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.