Mengapa Stack Ini?

Setiap komponen dipilih karena alasan spesifik:

Struktur Proyek Standard Kami

saas-project/
โ”œโ”€โ”€ frontend/                 # Next.js 14
โ”‚   โ”œโ”€โ”€ app/
โ”‚   โ”‚   โ”œโ”€โ”€ (dashboard)/      # Route group: halaman setelah login
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx    # Dashboard layout + auth check
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ page.tsx      # Main dashboard
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ settings/
โ”‚   โ”‚   โ”œโ”€โ”€ (auth)/           # Route group: login, register
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ login/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ register/
โ”‚   โ”‚   โ””โ”€โ”€ api/              # Next.js API Routes (BFF pattern)
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ ui/               # Reusable UI components
โ”‚   โ”‚   โ””โ”€โ”€ features/         # Feature-specific components
โ”‚   โ””โ”€โ”€ lib/
โ”‚       โ”œโ”€โ”€ api.ts            # API client
โ”‚       โ””โ”€โ”€ utils.ts
โ”‚
โ”œโ”€โ”€ backend/                  # FastAPI
โ”‚   โ”œโ”€โ”€ app/
โ”‚   โ”‚   โ”œโ”€โ”€ api/v1/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ auth.py
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ users.py
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ tenants.py
โ”‚   โ”‚   โ”œโ”€โ”€ models/           # SQLAlchemy ORM models
โ”‚   โ”‚   โ”œโ”€โ”€ schemas/          # Pydantic request/response schemas
โ”‚   โ”‚   โ””โ”€โ”€ core/
โ”‚   โ”‚       โ”œโ”€โ”€ config.py     # Settings dari .env
โ”‚   โ”‚       โ”œโ”€โ”€ security.py   # JWT, password hashing
โ”‚   โ”‚       โ””โ”€โ”€ database.py   # DB connection
โ”‚   โ””โ”€โ”€ alembic/              # Database migrations
โ”‚
โ””โ”€โ”€ docker-compose.yml        # Local development

Auth Pattern yang Aman: HTTP-only Cookie

Jangan gunakan localStorage untuk JWT โ€” ini kerentanan XSS yang umum. Gunakan HTTP-only cookie:

# FastAPI โ€” set cookie setelah login berhasil
@router.post("/auth/login")
async def login(credentials: LoginSchema, response: Response, db: Session = Depends(get_db)):
    user = authenticate_user(db, credentials.email, credentials.password)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    access_token = create_access_token(data={"sub": str(user.id)})

    response.set_cookie(
        key="auth_token",
        value=access_token,
        httponly=True,       # JavaScript tidak bisa akses
        secure=True,         # HTTPS only
        samesite="strict",   # CSRF protection
        max_age=7 * 24 * 3600  # 7 hari
    )
    return {"status": "ok", "user_id": user.id}

Multi-tenant dengan Schema PostgreSQL

Untuk setiap tenant baru, kita buat schema PostgreSQL tersendiri. Data benar-benar terisolasi tanpa perlu database terpisah:

-- PENTING: gunakan %1$I (positional) bukan %I saja
-- Bug umum yang susah di-debug!
CREATE OR REPLACE FUNCTION provision_tenant(tenant_slug TEXT)
RETURNS void AS $$
BEGIN
  -- Buat schema baru
  EXECUTE format('CREATE SCHEMA IF NOT EXISTS %1$I', tenant_slug);

  -- Buat tabel utama
  EXECUTE format('
    CREATE TABLE %1$I.users (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      email TEXT UNIQUE NOT NULL,
      role TEXT DEFAULT ''member'',
      created_at TIMESTAMPTZ DEFAULT NOW()
    )
  ', tenant_slug);

  EXECUTE format('
    CREATE TABLE %1$I.transactions (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      amount DECIMAL(15,2) NOT NULL,
      description TEXT,
      created_at TIMESTAMPTZ DEFAULT NOW()
    )
  ', tenant_slug);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Deployment: Vercel + VPS

Frontend โ†’ Vercel

Setup paling sederhana untuk Next.js. Connect GitHub repo, set environment variables, done. Preview deployment otomatis untuk setiap PR โ€” sangat membantu untuk review dengan klien.

# vercel.json โ€” konfigurasi minimal
{
  "buildCommand": "npm run build",
  "outputDirectory": ".next",
  "env": {
    "NEXT_PUBLIC_API_URL": "https://api.yourdomain.com"
  }
}

Backend โ†’ VPS dengan Nginx + PM2

Kami menggunakan VPS IDCloudHost Singapore (latensi rendah ke Indonesia). Setup standar:

# ecosystem.config.js untuk PM2
module.exports = {
  apps: [{
    name: 'saas-backend',
    script: 'uvicorn',
    args: 'app.main:app --host 0.0.0.0 --port 8000 --workers 2',
    interpreter: '/path/to/venv/bin/python',
    env: { NODE_ENV: 'production' },
    error_file: './logs/err.log',
    out_file: './logs/out.log'
  }]
}

# Nginx config
server {
    server_name api.yourdomain.com;
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Tips penting untuk Vercel deployment: Kalau ada config.js atau file yang perlu dimodifikasi saat build, gunakan buildCommand di vercel.json dengan sed injection daripada memodifikasi file langsung. Ini menghindari masalah file reverting ke versi lama setelah deployment.

Kapan Tidak Pakai Stack Ini?