Lompat ke konten utama
vourdev
Kembali ke Blog

Case Study

Bongkar Tech Stack Micro-SaaS Vour: Murah, Cepat, dan Siap Skala di Bawah $10/Bulan

Bedah tuntas arsitektur micro-SaaS Vour berbasis Next.js, Prisma, Neon, dan Cloudflare Workers yang tangguh menghadapi spike tanpa bikin kantong jebol.

Gambar Cover Bongkar Tech Stack Micro-SaaS Vour: Murah, Cepat, dan Siap Skala di Bawah $10/Bulan
Ditulis olehvourdev11 menit baca

Banyak founder teknis dan engineer terjebak di dua kutub ekstrem saat mulai membangun micro-SaaS: terlalu over-engineering dengan Kubernetes dan microservices rumit yang menghabiskan biaya infrastruktur $300 per bulan sebelum punya satu pun pengguna berbayar, atau terlalu kompromi menggunakan shared hosting murahan yang langsung tumbang begitu mendapat lonjakan traffic dari Product Hunt atau Twitter/X.

Di Vour Studio, kami menganut prinsip pragmatis: arsitektur harus sesederhana mungkin, biaya operasional mendekati nol di awal, tapi tidak boleh membutuhkan penulisan ulang total saat pengguna melonjak ke puluhan ribu *daily active users*.

Artikel ini membedah arsitektur produk yang kami gunakan di Vour Studio—mulai dari frontend rendering, lapisan ORM dan manajemen koneksi database, komputasi edge, hingga strategi caching—semuanya berjalan stabil dengan total cost infrastruktur di bawah $10 per bulan.

Tumpukan teknologi micro-SaaS Vour dari infrastruktur dasar hingga clientUI & Design SystemTailwind CSS + Radix UI / ShadcnApplication LayerNext.js App Router (Server Components & Actions)Edge & Background ComputeCloudflare Workers + Upstash RedisData LayerPrisma ORM + Neon Connection PoolingInfrastructureCloudflare Global Network & Neon ServerlessSemua layer menggunakan arsitektur serverless pay-as-you-go dengan free tier yang melimpah.
Tumpukan teknologi micro-SaaS Vour dari infrastruktur dasar hingga client

Anatomi Tech Stack: Mengapa Kombinasi Ini?

Sebelum masuk ke baris kode, mari kita lihat keputusan di balik pemilihan setiap komponen dalam stack ini.

  1. Next.js (App Router) & Tailwind CSS: Memberikan keseimbangan sempurna antara developer experience (DX), performa SEO melalui Server Components (RSC), dan efisiensi bundle size berkat Tailwind yang membuang CSS tak terpakai (purge otomatis).
  2. Prisma ORM & Neon (PostgreSQL Serverless): Memberikan type-safety end-to-end dari database hingga komponen UI tanpa perlu mengelola instance Linux atau patching database manual.
  3. Cloudflare Workers & Upstash: Bertindak sebagai shield di layer terdepan untuk rate limiting, caching, dan offloading background tasks yang terlalu berat untuk Next.js serverless functions standar.
Perbandingan Stack Tradisional vs Stack Edge Serverless VourTraditional VM / VPS StackSewa server $20-40/bulan sejakhari pertamaHarus setup Docker, Nginx, SSL,dan backup manualRisiko downtime saat hardware failSkalabilitas vertikal terbatas padaspek serverVour Edge Serverless StackBiaya $0 saat traffic sepi, <$10 saatmulai jalanDeploy otomatis via Git push tanpapusing OSMulti-region redundancy bawaanpenyedia cloudAuto-scale dari 0 ke ribuan requestsecara instan
Perbandingan Stack Tradisional vs Stack Edge Serverless Vour

Lapisan Data: Prisma ORM dan Serverless Postgres Tanpa Pusing Connection Pool

Visualisasi Prisma ORM schema dan query database modern
Konfigurasi schema Prisma yang dioptimasi untuk serverless environment. · Bozhin Karaivanov (Unsplash)

Masalah paling umum saat menjalankan aplikasi serverless seperti Next.js di Vercel adalah Connection Pool Exhaustion. Setiap kali fungsi serverless spin up, ia membuka koneksi baru ke database. Jika ada 200 concurrent requests, fungsi tersebut akan mencoba membuka 200 koneksi PostgreSQL sekaligus. Database standar seperti RDS atau VPS PostgreSQL biasa akan langsung crash karena kehabisan batas koneksi.

Kami mengatasi ini dengan mengombinasikan Neon Serverless PostgreSQL (yang memiliki built-in connection pooler berbasis PgBouncer) dengan Prisma ORM.

Berikut adalah konfigurasi prisma/schema.prisma yang kami optimalkan untuk lingkungan serverless:

prisma
// prisma/schema.prisma
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["driverAdapters"]
}

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")         // URL pooled untuk query aplikasi (PgBouncer)
  directUrl = env("DIRECT_URL")           // URL direct untuk migrasi skema (Prisma Migrate)
}

enum SubscriptionTier {
  FREE
  STARTER
  PRO
}

model User {
  id           String           @id @default(cuid())
  email        String           @unique
  name         String?
  tier         SubscriptionTier @default(FREE)
  createdAt    DateTime         @default(now())
  updatedAt    DateTime         @updatedAt
  projects     Project[]
  apiKeys      ApiKey[]

  @@index([email])
}

model Project {
  id          String   @id @default(cuid())
  name        String
  slug        String   @unique
  ownerId     String
  owner       User     @relation(fields: [ownerId], references: [id], onDelete: Cascade)
  isArchived  Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@index([ownerId])
  @@index([slug])
}

model ApiKey {
  id          String    @id @default(cuid())
  keyHash     String    @unique
  label       String
  userId      String
  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  lastUsedAt  DateTime?
  createdAt   DateTime  @default(now())

  @@index([userId])
}

Penjelasan Baris demi Baris:

  • `previewFeatures = ["driverAdapters"]`: Memungkinkan Prisma menggunakan adapter lightweight berbasis WebSocket atau HTTP untuk koneksi serverless yang jauh lebih hemat memori dibanding native binary engine.
  • `url` vs `directUrl`: url mengarah ke PgBouncer pooler (port 6543 di Neon) yang bertugas mendaur ulang koneksi aplikasi. Sementara directUrl mengarah langsung ke instance PostgreSQL (port 5432) karena perintah migrasi (prisma migrate deploy) membutuhkan session locks yang tidak didukung oleh mode transaction pooling.
  • `@index` pada foreign key & field pencarian: Menjaga query tetap berada dalam eksekusi sub-milidetik ketika tabel mulai membesar, menghindari full-table scan.

Agar inisialisasi Prisma Client tidak membuat instansiasi ganda setiap kali file di-compile ulang pada mode development, kami mengisolasi instance database dalam satu file singleton:

typescript
// lib/db.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = db;
}
Tips Praktis: Jangan pernah menjalankan prisma.user.findMany() tanpa take (limit) dan select spesifik di serverless environment. Membatasi ukuran payload JSON yang keluar dari database langsung memotong konsumsi memori fungsi serverless hingga 40%.

Integrasi Application Layer: Server Actions yang Type-Safe

Di Next.js App Router, kami meninggalkan arsitektur REST API konvensional untuk kebutuhan internal dashboard dan beralih ke Server Actions. Pendekatan ini menghilangkan kebutuhan membuat route handler terpisah, serialisasi manual, dan penulisan fetch client boilerplate.

Berikut contoh pola implementasi pembuatan entitas baru yang aman, dilengkapi validasi schema via Zod dan transaksi database atomic:

typescript
// app/actions/create-project.ts
"use server";

import { z } from "zod";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

// 1. Skema validasi input
const CreateProjectSchema = z.object({
  name: z.string().trim().min(3, "Nama project minimal 3 karakter").max(50, "Maksimal 50 karakter"),
  slug: z
    .string()
    .trim()
    .toLowerCase()
    .min(3)
    .max(30)
    .regex(/^[a-z0-9-]+$/, "Slug hanya boleh berisi huruf kecil, angka, dan strip"),
  userId: z.string().cuid("User ID tidak valid"),
});

export type ActionResponse<T = null> = {
  success: boolean;
  data?: T;
  error?: string;
};

export async function createProjectAction(formData: FormData): Promise<ActionResponse<{ id: string; slug: string }>> {
  try {
    // 2. Parse dan validasi data secara aman
    const rawData = {
      name: formData.get("name"),
      slug: formData.get("slug"),
      userId: formData.get("userId"),
    };

    const validated = CreateProjectSchema.safeParse(rawData);

    if (!validated.success) {
      const firstError = validated.error.issues[0]?.message ?? "Input tidak valid";
      return { success: false, error: firstError };
    }

    const { name, slug, userId } = validated.data;

    // 3. Eksekusi query dengan proteksi duplikasi
    const existingProject = await db.project.findUnique({
      where: { slug },
      select: { id: true },
    });

    if (existingProject) {
      return { success: false, error: "Slug sudah digunakan oleh project lain." };
    }

    const newProject = await db.project.create({
      data: {
        name,
        slug,
        ownerId: userId,
      },
      select: {
        id: true,
        slug: true,
      },
    });

    // 4. Invalidate cache Next.js agar UI terupdate instan
    revalidatePath("/dashboard/projects");

    return { success: true, data: newProject };
  } catch (err) {
    console.error("[ACTION_CREATE_PROJECT_ERROR]", err);
    return { success: false, error: "Terjadi kesalahan internal pada server." };
  }
}

Detail Alur Kerja Kode di Atas:

  1. `"use server"`: Menandai bahwa fungsi ini hanya akan dieksekusi di sisi server. Kompiler Next.js secara otomatis membuat endpoint RPC terenkripsi di balik layar.
  2. `z.safeParse()`: Memvalidasi payload sebelum menyentuh database. Menghindari SQL injection secara implisit dan menangani validasi bisnis secara deklaratif.
  3. `select` spesifik: Kami hanya menarik field id dan slug saat insert, bukan seluruh baris data. Ini meminimalkan alokasi memori runtime.
  4. `revalidatePath()`: Menginstruksikan router Next.js untuk me-refresh data cache di /dashboard/projects tanpa perlu melakukan full page reload.

Edge Layer: Cloudflare Workers untuk Heavy Lifting dan Rate Limiting

Cloudflare Workers edge network compute architecture
Cloudflare Workers menangani caching dan proteksi bot tepat di edge. · Growtika (Unsplash)

Ketika micro-SaaS Anda mulai menyediakan Public API atau menerima incoming webhooks (misalnya dari payment gateway seperti Stripe atau Midtrans), membiarkan semua traffic masuk ke fungsi serverless Next.js adalah pemborosan kuota komputasi.

Cloudflare Workers adalah senjata rahasia kami. Dengan waktu cold start mendekati 0 milidetik dan jangkauan edge network global, Workers ideal untuk:

  • Memvalidasi signature webhook.
  • Menolak bot dan request jahat via IP rate-limiting.
  • Melakukan caching respons publik.
Alur pemrosesan request dari browser/API client melalui Cloudflare Edge hingga DatabaseClient RequestBrowser / API ConsumerCloudflare WorkerEdge Rate Limiter & Auth GuardNext.js App ServerServerless Compute (Vercel)Neon PostgreSQLConnection PoolerHTTPS RequestAuthorized & CleanPrisma Query via PoolerDatasetHTML / JSON Stream
Alur pemrosesan request dari browser/API client melalui Cloudflare Edge hingga Database

Berikut adalah implementasi Worker TypeScript mandiri yang kami gunakan sebagai gerbang API terdepan dengan sliding-window rate limiting menggunakan Upstash Redis:

typescript
// src/worker.ts (Cloudflare Workers)
export interface Env {
  UPSTASH_REDIS_REST_URL: string;
  UPSTASH_REDIS_REST_TOKEN: string;
  NEXTJS_BACKEND_URL: string;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const clientIp = request.headers.get("cf-connecting-ip") ?? "127.0.0.1";

    // 1. Jalankan Rate Limiter hanya pada endpoint publik /api/v1/*
    if (url.pathname.startsWith("/api/v1/")) {
      const rateLimitKey = `rate-limit:${clientIp}:${Math.floor(Date.now() / 60000)}`;
      
      // Request ke Upstash REST API langsung via HTTP fetch (tanpa TCP overhead)
      const redisUrl = `${env.UPSTASH_REDIS_REST_URL}/incr/${rateLimitKey}`;
      const redisRes = await fetch(redisUrl, {
        headers: { Authorization: `Bearer ${env.UPSTASH_REDIS_REST_TOKEN}` },
      });

      if (redisRes.ok) {
        const json = (await redisRes.json()) as { result: number };
        const requestCount = json.result;

        // Set expire key ke 60 detik jika baru pertama kali dibuat
        if (requestCount === 1) {
          ctx.waitUntil(
            fetch(`${env.UPSTASH_REDIS_REST_URL}/expire/${rateLimitKey}/60`, {
              headers: { Authorization: `Bearer ${env.UPSTASH_REDIS_REST_TOKEN}` },
            })
          );
        }

        // Batasi maksimal 60 request per menit per IP
        if (requestCount > 60) {
          return new Response(
            JSON.stringify({ error: "Terlalu banyak request. Coba lagi dalam 1 menit." }),
            {
              status: 429,
              headers: { "Content-Type": "application/json", "Retry-After": "60" },
            }
          );
        }
      }
    }

    // 2. Jika lolos rate limit, teruskan request ke backend Next.js
    const targetUrl = new URL(url.pathname + url.search, env.NEXTJS_BACKEND_URL);
    const modifiedRequest = new Request(targetUrl.toString(), {
      method: request.method,
      headers: request.headers,
      body: request.body,
      redirect: "follow",
    });

    return fetch(modifiedRequest);
  },
};

Mengapa Pendekatan Ini Sangat Murah dan Efisien?

  1. `ctx.waitUntil()`: Perintah untuk mengeksekusi expiration Redis secara asinkron di latar belakang tanpa menahan waktu respons yang diterima pengguna.
  2. Upstash REST API: Menggunakan HTTP alih-alih koneksi TCP socket tradisional, sangat cocok untuk arsitektur serverless yang stateless.
  3. Pencegahan Beban Server: Sebanyak 80-90% serangan DDoS mikro atau abusive scrapers dipangkas langsung di jaringan edge Cloudflare sebelum sempat mengonsumsi kuota eksekusi Next.js atau query database Anda.

Rincian Biaya Nyata (Real-World Monthly Cost Breakdown)

Mari kita bedah angka konkret pengeluaran bulanan untuk menjalankan micro-SaaS dengan arsitektur ini pada skala 0 hingga 50.000 monthly active users (MAU):

  • Next.js Hosting (Vercel Hobby / Cloudflare Pages): $0.00 / bulan (Mencakup 100GB bandwidth dan jutaan eksekusi fungsi gratis).
  • Neon Serverless PostgreSQL: $0.00 / bulan (Free tier mencakup 0.5 GB storage, komputasi terpisah, dan connection pooler bawaan).
  • Cloudflare Workers (Edge Compute & DNS): $0.00 - $5.00 / bulan (Free tier 100.000 request/hari gratis; upgrade ke Workers Paid hanya $5/bulan jika butuh 10 juta request).
  • Upstash Redis (Rate Limiting & Queue): $0.00 / bulan (Free tier 10.000 commands/hari gratis).
  • Custom Domain (.dev / .com): ~$1.00 / bulan (Dihitung amortisasi tahunan $12/tahun).

Total Estimasi Biaya: $1.00 - $6.00 / bulan.

Anda mendapatkan infrastruktur berskala global, zero maintenance, auto-scaling, dan backup otomatis dengan biaya yang lebih murah dari secangkir kopi specialty.

Anti-Pattern dan Gotchas yang Harus Dihindari

Berdasarkan pengalaman kami membangun dan memelihara produk di Vour Studio, ada beberapa jebakan yang sering menjerat tim saat mengadopsi stack ini:

  1. Mengabaikan Cold Start Neon: Neon mematikan compute endpoint setelah beberapa menit tidak aktif pada tier gratis (auto-suspend). Ini bisa menyebabkan jeda 500-1000ms pada request pertama. Solusinya: pasang cron job ringan di Cloudflare Workers yang melakukan SELECT 1 setiap 5 menit untuk menjaga instance tetap hangat jika produk Anda membutuhkan respons real-time.
  2. Menempatkan Secret di Server Action Tanpa Proteksi Auth: Server Action adalah endpoint publik secara teknis. Jangan berasumsi bahwa karena ditulis di file Next.js, fungsinya otomatis aman. Selalu periksa sesi pengguna (auth()) di baris pertama setiap Server Action.
  3. Memakai Prisma Migrate di Serverless Runtime: Jangan pernah memanggil prisma.$connect() atau fungsi DDL migrasi di dalam runtime handler serverless. Migrasi skema harus selalu dijalankan melalui CI/CD pipeline (seperti GitHub Actions) saat fase deployment.

Kesimpulan & Action Items

Membangun micro-SaaS yang tangguh tidak harus mahal atau rumit. Dengan memadukan Next.js App Router, Prisma, Neon PostgreSQL, dan Cloudflare Workers, Anda mendapatkan fondasi teknis kelas satu: cepat saat didevelop, murah saat divalidasi, dan siap menampung lonjakan pengguna tanpa perlu merombak ulang seluruh kode.

Langkah Praktis yang Bisa Anda Lakukan Sekarang:

  1. Audit Dependency Anda: Singkirkan container setup yang mahal jika aplikasi Anda masih berada di fase validasi MVP.
  2. Ganti Direct Postgres Connection: Aktifkan PgBouncer atau gunakan driver serverless (seperti @neondatabase/serverless) untuk menghentikan masalah connection drop.
  3. Pasang Edge Protection: Tempatkan Cloudflare Worker sederhana di depan API Anda untuk memotong traffic liar sebelum menyentuh backend utama.
  4. Deploy Sekarang: Bangun MVP Anda, batasi ruang lingkup fitur, dan biarkan infrastruktur serverless menangani beban operasionalnya.

Butuh penyesuaian khusus untuk project Anda?

Jika situasi operasional atau arsitektur sistem bisnis Anda membutuhkan solusi kustom, diskusikan langsung bersama tim engineer kami. Anda juga bisa melihat rincian layanan vour.dev atau menghitung estimasi biaya project lebih dulu.

Mulai Project