/saas — 풀스택 SaaS 빌더
트리거: /saas [아이디어] 또는 /saas (아이디어 입력 프롬프트)
결과물: Vercel URL에 완전히 배포된 프로덕션 준비 SaaS. 작동하는 인증, 실제 백엔드, 실시간 데이터, 아름다운 UI. 데모가 아닙니다. 스캐폴드가 아닙니다. 실제 제품입니다.
파이프라인 개요
INTAKE → RESEARCH (병렬) → PLAN → CLARIFY → BUILD → TEST → REVIEW → DEPLOY
각 단계가 다음 단계를 게이트합니다. 지름길 없음. 건너뛰기 없음.
PHASE 1 — INTAKE
사용자가 /saas [아이디어]를 실행하면 명령어에서 아이디어를 추출합니다.
인수 없이 /saas를 실행하면 다음을 질문합니다:
SaaS 아이디어는 무엇인가요? 1-3문장으로 설명하세요. 포함:
- 무엇을 하는가
- 누구를 위한 것인가
- 어떤 문제를 해결하는가
IDEA로 저장합니다. 진행하기 전에 한 문장으로 확인합니다.
PHASE 2 — PARALLEL RESEARCH
Agent 도구를 사용하여 두 개의 연구 트랙을 동시에 시작합니다. 하나가 끝날 때까지 기다리지 마십시오.
Track A — 시장 정보
다음 프롬프트로 general-purpose 에이전트를 생성합니다:
이 아이디어에 대한 SaaS 시장을 조사하세요: [IDEA]
다음을 포함하는 구조화된 보고서를 제공하세요:
1. 기존 플레이어 (5-8개 경쟁사)
- 이름, URL, 가격 티어, 포지셔닝
- 잘하는 점, 놓치는 점
- 가능한 경우 예상 ARR
2. 시장 규모 및 수요
- 카테고리 (CRM, 프로젝트 관리, 분석 등)
- 시장 규모 추정
- 성장 추세 (성장/정체/쇠퇴)
- 수요 증거 (Reddit 스레드, ProductHunt upvotes, 구인 공고)
3. 가격 벤치마크
- 프리미엄 vs 유료 전용 vs 무료 체험
- 일반적인 가격대 ($/사용자/월, 정액, 사용량 기반)
- 이 카테고리에서 어떤 모델이 성공하는가
4. 기능 테이블
- 경쟁사 제품에서 가장 일반적인 10가지 기능 목록
- 기본 필수 기능과 차별화 요소 구분
5. 블루 오션
- 경쟁사가 하지 않는 것은 무엇인가?
- 사용자들은 어디에서 불평하는가? (G2, Capterra, Reddit 확인)
- 틈새 기회는 무엇인가?
구조화된 마크다운을 반환하세요. 구체적으로 작성하세요. 불필요한 내용은 없이.
Track B — 기술 패턴 연구
두 번째 general-purpose 에이전트를 생성합니다:
이 유형의 SaaS에 대한 기술 구현 패턴을 조사하세요: [IDEA]
다음을 제공하세요:
1. 데이터 모델 패턴
- 이 유형의 앱에 일반적으로 필요한 테이블/엔티티는 무엇인가?
- 주요 관계 및 제약 조건
- 멀티 테넌시 접근 방식 (행 수준 org_id vs 별도 스키마)
2. 인증 패턴
- 이 유형의 앱에 소셜 로그인, 매직 링크, SSO가 필요한가?
- 초대 전용 vs 공개 가입?
- 일반적인 역할 패턴 (admin/member/viewer?)
3. 결제 패턴
- 사용자당, 사용량 기반, 또는 정액?
- 이 분야에서 효과적인 무료 티어 전략?
4. 통합 필요 사항
- 이 카테고리의 앱은 일반적으로 무엇과 통합되는가?
- Webhooks, Zapier, API 키?
5. 기술적 위험
- 이 카테고리에서 구축하기 어려운 것은 무엇인가?
- 팀이 흔히 실수하는 것은 무엇인가?
- 성능/확장 고려 사항
구조화된 마크다운을 반환하세요. 구체적으로 작성하세요.
두 트랙이 모두 완료될 때까지 기다린 후 진행합니다.
PHASE 3 — ARCHITECTURE PLAN
두 연구 결과를 사용하여 구조화된 빌드 계획을 생성합니다. 이를 .saas-workspace/[project-slug]/PLAN.md에 작성합니다.
계획에는 다음이 포함되어야 합니다:
3.1 제품 정의
## 제품
이름: [아이디어에서 파생, 변경 가능]
태그라인: [한 줄]
핵심 가치 제안: [한 문장]
대상 사용자: [특정 페르소나]
## MVP 기능 세트 (오늘 구축할 것)
- [기능 1] — [범위에 포함된 이유]
- [기능 2] — [범위에 포함된 이유]
...
## 범위 제외 (v1)
- [기능] — [v2로 미루는 이유...]
3.2 데이터 모델
완전한 Postgres 스키마를 설계합니다. 모든 테이블에 대해:
- 테이블 이름, 목적
- 모든 컬럼 (타입, 제약 조건, 기본값)
- 외래 키 관계
- 필요한 RLS 정책
- 인덱스
다음 필수 테이블로 시작합니다:
-- 항상 포함:
profiles (auth.users 확장)
organizations (멀티 테넌트인 경우)
organization_members (멀티 테넌트인 경우)
subscriptions (결제가 있는 경우)
그런 다음 제품에 대한 도메인별 테이블을 추가합니다.
3.3 경로 맵
랜딩 페이지: /
인증: /login /signup /auth/callback
앱 셸: /app (인증 필요)
핵심 기능: /app/[feature] (MVP 기능당 하나)
설정: /app/settings
API 라우트: /api/... (각각 나열)
3.4 기술 스택 확인
프레임워크: Next.js 15 (App Router, TypeScript)
데이터베이스: Supabase (Postgres + Auth + RLS)
스타일링: Tailwind CSS v4 + shadcn/ui
이메일: Resend (트랜잭셔널)
배포: Vercel
결제: Stripe (결제가 필요한 경우)
3.5 빌드 순서
정확한 순서를 나열: 마이그레이션 → 인증 → API 라우트 → 앱 셸 → 기능 → 랜딩 페이지 → 이메일 템플릿 → 테스트.
PHASE 4 — CLARIFYING QUESTIONS
코드를 한 줄도 작성하기 전에, 격차를 표면화합니다. AskUserQuestion을 사용하여 다음을 질문합니다 (호출당 최대 4개 질문으로 그룹화):
Round 1 — 제품 범위:
- 이름 확인 (아이디어에서 파생된 3가지 옵션 제시)
- 멀티 테넌트(팀/조직) 또는 단일 사용자 계정?
- v1에 결제/빌링이 필요한가, 아니면 구축 중에는 무료인가?
- 연구 결과에서 반드시 필요한 기능과 있으면 좋은 기능?
Round 2 — 디자인 및 배포 (Round 1의 답변이 새로운 질문을 제기하는 경우에만 질문):
- 도메인? (또는 지금은 Vercel 서브도메인 사용)
- 브랜드 색상/선호 사항이 있는가? (기본값: Rawgrowth 다크 그린 시스템)
- Resend용 이메일 발신자 주소?
진행하기 전에 PLAN.md에 답변을 통합합니다.
PHASE 5 — BUILD
사전 빌드 설정
코드를 작성하기 전에 다음 스킬을 로드합니다:
frontend-theme— 전체 Rawgrowth 디자인 시스템 주입ui-ux-pro-max— 디자인 결정, 팔레트, 타이포그래피- 복잡한 컴포넌트를 위해 Magic MCP (
@21st-dev/magic) 호출
프로젝트 위치: ~/saas-projects/[project-slug]/에 생성
스캐폴드 명령어:
cd ~/saas-projects
npx create-next-app@latest [project-slug] \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--no-git
cd [project-slug]
의존성 설치:
npm install @supabase/supabase-js @supabase/ssr \
resend \
@radix-ui/react-icons \
lucide-react \
class-variance-authority clsx tailwind-merge \
zod \
@hookform/resolvers react-hook-form
# shadcn/ui 초기화
npx shadcn@latest init --defaults
# shadcn 컴포넌트 추가
npx shadcn@latest add button input label card \
dropdown-menu avatar badge separator toast \
dialog sheet tabs form
Tailwind v4 설정 — src/app/globals.css에 frontend-theme 토큰 적용.
5.1 데이터베이스 및 마이그레이션
각 마이그레이션에 대해 add-migration 스킬을 사용합니다.
마이그레이션 1: 코어 스키마
-- 모든 사용자 대상 테이블에 RLS 활성화
-- profiles: auth.users 확장
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
email TEXT NOT NULL,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "사용자는 자신의 프로필을 볼 수 있음"
ON profiles FOR SELECT USING (auth.uid() = id);
CREATE POLICY "사용자는 자신의 프로필을 업데이트할 수 있음"
ON profiles FOR UPDATE USING (auth.uid() = id);
-- 가입 시 프로필 자동 생성
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name, avatar_url)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
마이그레이션 2: 조직 (멀티 테넌트인 경우)
CREATE TABLE organizations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
owner_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
plan TEXT DEFAULT 'free',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE organization_members (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(org_id, user_id)
);
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
CREATE POLICY "멤버는 자신의 조직을 볼 수 있음"
ON organizations FOR SELECT
USING (id IN (
SELECT org_id FROM organization_members WHERE user_id = auth.uid()
));
CREATE POLICY "멤버는 조직 멤버십을 볼 수 있음"
ON organization_members FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members WHERE user_id = auth.uid()
));
마이그레이션 3+: 도메인 테이블 — PLAN.md의 데이터 모델에 따라 제품별 테이블을 생성합니다. 모든 테이블에 RLS를 적용합니다.
5.2 Supabase 클라이언트 설정
src/lib/supabase/client.ts:
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
src/lib/supabase/server.ts:
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 {}
},
},
}
)
}
src/middleware.ts — 모든 /app/* 라우트를 보호하고, 인증되지 않은 사용자를 /login으로 리디렉션합니다.
5.3 인증 페이지
/login 및 /signup을 전체 페이지(모달 아님)로 구축합니다. frontend-theme 적용:
- 어두운 배경 (#060B08)
- 중앙 정렬 카드 (테두리: rgba(255,255,255,0.06))
- 녹색 CTA 버튼 (#0CBF6A)
- 이메일 + 비밀번호 필드 (+ 설정된 경우 소셜 로그인)
- "비밀번호 찾기" 링크
- 이모지 없음. 깔끔하고, 프리미엄하며, 미니멀합니다.
src/app/(auth)/login/page.tsx — Supabase Auth를 사용한 로그인 폼
src/app/(auth)/signup/page.tsx — 이메일 확인 흐름이 있는 가입 폼
src/app/auth/callback/route.ts — OAuth/매직 링크 콜백 핸들러
이메일 확인 흐름:
- 가입 시 → Supabase가 확인 이메일 전송 (Resend SMTP 사용)
- 콜백 라우트가 코드를 세션으로 교환
- 성공 시
/app/dashboard로 리디렉션
5.4 앱 셸
src/app/(app)/layout.tsx — 인증된 레이아웃:
- 사이드바 (모바일에서 접을 수 있음)
- 사용자 아바타 + 드롭다운이 있는 상단 내비게이션 (프로필, 설정, 로그아웃)
- 브레드크럼
- 토스트 알림 아울렛
사이드바 항목은 PLAN.md의 경로 맵에서 파생됩니다.
Magic MCP를 사용하여 사이드바 컴포넌트를 가져옵니다: "dashboard sidebar dark"를 검색하고 frontend-theme 토큰에 맞게 조정합니다.
5.5 핵심 기능 페이지
MVP 기능 세트 (PLAN.md)의 각 기능에 대해:
- 먼저 디자인 — Magic MCP에 관련 컴포넌트 패턴 질의
- 데이터 계층 — 서버 컴포넌트가 Supabase에서 직접 가져오기
- 변경 — Zod 유효성 검사가 있는 Server Actions
- 오류 상태 — 모든 폼에 유효성 검사 오류, 로딩 상태, 빈 상태
any타입 없음 — Supabase 스키마에서 TypeScript 타입 생성
데이터 가져오기 서버 컴포넌트 패턴:
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
redirect('/login')
}
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', session.user.id)
.single()
return (
<div>
<h1>대시보드</h1>
<pre>{JSON.stringify(profile, null, 2)}</pre>
</div>
)
}