Back to blog

Vercel, Supabase & the Modern Developer Ecosystem

vercelsupabasenextjsfullstackcloudtypescript
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

PrincipleTraditional CloudModern Ecosystem
Setup timeHours to daysMinutes
ConfigurationManual / IaCConvention over configuration
ScalingManual / auto-scaling policiesAutomatic, serverless
PricingPay for resources (running 24/7)Pay per usage (scale to zero)
DeploymentCI/CD pipelines, Docker, K8sgit push
DatabaseProvision and manage yourselfManaged with built-in branching
Free tierLimited (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:

  1. Vercel automatically detects the framework (Next.js, Vite, Nuxt, Remix, etc.)
  2. Runs the build command
  3. Deploys static assets to the global CDN
  4. Deploys API routes / server components as serverless functions
  5. 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.app

Serverless 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

FeatureHobby (Free)Pro ($20/mo)Enterprise
DeploymentsUnlimitedUnlimitedUnlimited
Bandwidth100 GB1 TBCustom
Serverless Execution100 GB-hours1000 GB-hoursCustom
Preview Deployments
AnalyticsBasicAdvancedAdvanced
Team Members1UnlimitedUnlimited
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

FeatureVercelNetlifyCloudflare Pages
Best ForNext.js appsStatic sites, JAMstackEdge-first apps
Framework SupportAll (Next.js is first-class)AllAll
Edge Functions✅ Edge Middleware✅ Edge Functions✅ Workers
Serverless Functions✅ (Node.js, Go, Python)✅ (Node.js)✅ (Workers)
Build SpeedFastFastFast
Free Bandwidth100 GB100 GBUnlimited
ISR SupportNativeVia pluginsVia Workers
AnalyticsBuilt-inVia pluginBuilt-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

FeatureSupabaseFirebaseNeonPlanetScale
DatabasePostgreSQLFirestore (NoSQL)PostgreSQLMySQL
Open Source
SQL SupportFull SQLNo SQLFull SQLFull SQL
AuthBuilt-inBuilt-in❌ (use third-party)❌ (use third-party)
RealtimeBuilt-inBuilt-in
Edge Functions✅ (Deno)✅ (Node.js)
StorageBuilt-inBuilt-in
RLSSecurity Rules✅ (manual)
Branching
Self-Host
Free Tier DB500 MB1 GB512 MB5 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

FeatureFreePro ($25/mo)Team ($599/mo)
Database500 MB8 GB16 GB
Bandwidth5 GB250 GB500 GB
Storage1 GB100 GB200 GB
Edge Functions500K invocations2M invocations5M invocations
Auth MAUs50,000100,000100,000+
Realtime200 concurrent500 concurrentCustom
SupportCommunityEmailPriority

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! }),
  ],
});
FeatureSupabase AuthClerkAuth.js
PriceFree (50K MAUs)Free (10K MAUs)Free (open source)
UI ComponentsBasicBeautiful, pre-builtBuild your own
MFAVia providers
Organizations
Self-hosted
Best ForSupabase projectsPolished auth UXCustom 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 publishedPosts

Prisma

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 },
});
FeatureDrizzlePrisma
ApproachCode-first (TypeScript)Schema-first (SDL)
Bundle Size~50 KB~800 KB
SQL-like Syntax❌ (own API)
MigrationsSQL migrationsPrisma Migrate
Raw SQLEasy$queryRaw
Edge Support✅ (with adapters)
Learning CurveLow (if you know SQL)Low (intuitive API)
Best ForSQL-savvy devsTeams 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

ToolCategoryWhat It DoesFree Tier
NeonDatabaseServerless PostgreSQL with branching512 MB
TursoDatabaseSQLite at the edge (libSQL)9 GB storage
Lemon SqueezyPaymentsStripe alternative (handles tax/billing)Free (fees per transaction)
InngestBackground JobsEvent-driven serverless functions25K events/mo
Trigger.devBackground JobsOpen-source serverless background jobs50K runs/mo
PostHogAnalyticsProduct analytics, feature flags, session replay1M events/mo
SentryMonitoringError tracking, performance monitoring5K 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-kit

Environment 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-deploys

Part 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

LimitationDetailsMitigation
Vendor lock-inVercel-specific features (Edge Middleware, ISR)Keep core logic framework-agnostic
Cold startsServerless functions have startup latency (50-500ms)Use edge functions, keep functions warm
Egress costsBandwidth can get expensive at scaleCache aggressively, optimize payloads
Scaling ceilingServerless has concurrency limitsContact sales for higher limits
Limited computeNo long-running processes, no GPUUse traditional cloud for heavy compute
Database sizeFree 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-app

Supabase → 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:

ToolFree TierWhat You Get
VercelHobbyUnlimited deployments, 100 GB bandwidth
SupabaseFree500 MB database, 50K auth users, 1 GB storage
ClerkFree10,000 monthly active users
UpstashFree10K commands/day Redis, 500 messages/day QStash
ResendFree3,000 emails/month
StripeFreePay only per-transaction fees (2.9% + $0.30)
SentryFree5,000 errors/month
PostHogFree1M events/month
Total$0/monthEnough for thousands of users

Growth Stack ($50-100/month)

When you outgrow free tiers:

ToolPlanCost
VercelPro$20/month
SupabasePro$25/month
ClerkPro$25/month (10K+ MAUs)
UpstashPay-as-you-go~$5-10/month
ResendPro$20/month
Total~$95-100/month

This stack can handle tens of thousands of active users with excellent performance.

Comparison with Traditional Cloud

Modern StackAWS Equivalent
ComputeVercel ($20)EC2 / ECS (~$30-100)
DatabaseSupabase ($25)RDS (~$30-60)
AuthClerk ($25)Cognito (~$10-50) + dev time
CDNIncluded in VercelCloudFront (~$10-30)
RedisUpstash ($5)ElastiCache (~$15-50)
EmailResend ($20)SES (~$5) + dev time
MonitoringPostHog/Sentry (free)CloudWatch (~$10-30)
DevOps timeMinimalSignificant
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-schema

This 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_KEY only in server-side code
  • ✅ Validate webhook signatures (Stripe, Clerk)
  • ✅ Rate limit all API endpoints
  • ✅ Set proper CORS headers
  • ✅ Use httpOnly cookies 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:

ComponentRecommended ToolWhy
DeploymentVercelBest Next.js integration, preview deployments
DatabaseSupabasePostgreSQL + auth + realtime in one package
AuthClerk or Supabase AuthClerk for advanced UX, Supabase Auth for simplicity
ORMDrizzleLightweight, type-safe, SQL-like
CachingUpstash RedisServerless, pay-per-request
EmailResendDeveloper-first API, React Email templates
PaymentsStripeIndustry standard, excellent docs
AnalyticsPostHogOpen-source, generous free tier
MonitoringSentryError 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

📬 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.