배포
pnpm build가 dist/를 만들었고 supabase functions deploy로 Edge Function이 Supabase 프로젝트에 올라갔다는 가정 위에서 시작합니다. 여기서는 그 정적 산출물과 함수를 사용자 브라우저까지 보내는 네 갈래 경로를 봅니다.
build-and-package까지가 도구가 자동으로 해 주는 영역입니다. 호스팅 선택과 custom domain, Stripe 운영 환경 webhook 등록, CSP connect-src 채우기는 전부 자동화 밖입니다. 운영 환경 키와 DNS, 결제 콘솔은 사용자 자격으로 움직여야 하는 자산이라, 도구가 그것을 대신 누르면 비밀 값이 노출되거나 운영 환경에 잘못된 endpoint가 등록됩니다.
네 경로 한눈에
섹션 제목: “네 경로 한눈에”| 경로 | 빌드 자동 | env 주입 | Edge Function | 적합한 경우 |
|---|---|---|---|---|
| Vercel | git push 시 자동으로 빌드합니다 | 대시보드 또는 vercel env 를 씁니다 | Supabase 쪽에 둡니다 | 기본으로 추천하며 가장 손이 덜 갑니다 |
| Netlify | git push 시 자동으로 빌드합니다 | 대시보드 또는 netlify env:import 를 씁니다 | Supabase 쪽에 둡니다 | Vercel 대신 쓸 때 고릅니다 |
| Cloudflare Pages | git push 시 자동으로 빌드합니다 | 대시보드 또는 wrangler pages secret 을 씁니다 | Supabase 또는 Cloudflare Workers 에 둡니다 | edge 인프라와 통합할 때 고릅니다 |
| self-host | 수동으로 하거나 CI 를 씁니다 | 호스팅 환경에 직접 넣습니다 | Supabase 쪽에 둡니다 | 사내 정책상 외부 호스팅이 불가할 때 고릅니다 |
네 경로는 배타적이지 않습니다. staging은 Vercel preview deploy로 두고 운영 환경은 self-host로 나눠도 됩니다. 한쪽을 끊고 다른 쪽으로 넘기는 마이그레이션도 흔합니다.
Supabase 프로젝트 분리
섹션 제목: “Supabase 프로젝트 분리”호스팅 선택보다 먼저 결정할 것은 Supabase 프로젝트 분리입니다. 운영 환경과 staging을 같은 Supabase 프로젝트로 두면 행 수준 접근 정책을 한 줄 잘못 쓴 실수가 곧장 실사용자 데이터에 닿습니다.
Supabase 조직├── saas-base-production ← 실사용자와 Stripe live key 를 씁니다└── saas-base-staging ← 개발자 테스트와 Stripe test key 를 씁니다각 프로젝트가 자기 SUPABASE_URL, ANON_KEY, SERVICE_ROLE_KEY, STRIPE_*_KEY를 가지고 있습니다. 호스팅 환경의 운영 환경 env와 preview 또는 staging env가 서로 다른 프로젝트를 가리키게 설정합니다.
마이그레이션은 같은 SQL을 양쪽에 적용합니다.
supabase link --project-ref staging-refsupabase db push # staging 에 먼저 적용합니다# 확인한 다음에supabase link --project-ref production-refsupabase db push # 운영 환경에 적용합니다화면에서 정책을 만들지 않는다는 규칙(architecture의 행 수준 접근 정책 기준 문서)이 여기서도 함께 적용됩니다. 같은 마이그레이션이 두 환경에 같은 모양으로 들어가야 환경 사이에 차이가 생기지 않습니다.
Vercel
섹션 제목: “Vercel”vercel link # 프로젝트를 연결합니다vercel env pull .env.local # 원격 env 를 로컬로 받아 옵니다vercel deploy --prod # 운영 환경으로 배포합니다vercel link를 한 번 실행한 다음에는 git push로 preview deploy가 자동으로 만들어집니다. 운영 환경 배포는 main 브랜치 push 또는 vercel deploy --prod로 직접 호출합니다.
env 주입은 대시보드의 Settings → Environment Variables에서 하거나 CLI로 합니다.
vercel env add VITE_SUPABASE_URL productionvercel env add VITE_SUPABASE_ANON_KEY productionvercel env add VITE_STRIPE_PUBLISHABLE_KEY productionSUPABASE_SERVICE_ROLE_KEY와 STRIPE_SECRET_KEY는 여기에 넣지 않습니다. 그 둘은 Edge Function 쪽에 supabase secrets set으로 등록합니다. 호스팅 env에 넣으면 SPA 번들로 노출되지는 않더라도 build 시점에 노출될 위험이 생깁니다.
custom domain은 대시보드의 Domains 메뉴에서 설정합니다. DNS A 또는 CNAME 레코드를 한 줄 넣으면 되고, Vercel이 SSL 인증서를 자동으로 발급합니다.
Netlify
섹션 제목: “Netlify”netlify linknetlify env:import .env.productionnetlify deploy --prodnetlify env:import가 .env.production 파일을 통째로 읽어 들입니다. .env.production은 운영 환경 키만 들어 있는 파일이고 .gitignore에 있어야 합니다. 가져온 다음에는 그 파일을 지우는 편이 안전합니다.
build 명령은 netlify.toml에 적습니다.
[build] command = "pnpm build" publish = "dist"custom domain과 SSL은 Vercel과 같은 흐름입니다.
Cloudflare Pages
섹션 제목: “Cloudflare Pages”wrangler pages deploy distCloudflare Pages는 git push로 자동 deploy를 실행하거나 wrangler pages deploy dist로 직접 올립니다. 두 방법 모두 잘 동작합니다.
env 주입은 대시보드에서 하거나 wrangler로 합니다.
wrangler pages secret put VITE_SUPABASE_URL --project-name=saas-baseCloudflare 환경의 강점은 Workers와의 통합입니다. Edge Function을 Supabase 쪽이 아닌 Cloudflare Workers로 옮기고 싶다면 api/adapter-hono/의 Hono 어댑터로 교체합니다. VITE_API_MODE=hono로 두면 client는 fetch('/api/...')로 호출하고, 그 path가 Workers의 라우트로 연결됩니다.
이 분기는 phase 4(/4-design-saas)의 API 어댑터 결정 단계에서 정합니다. 결정 결과는 .claude/state/api-adapter.json에 저장됩니다. 한 번 정해진 다음 교체할 때는 features 코드를 바꾸지 않고 어댑터 하나만 바꾸면 됩니다.
self-host
섹션 제목: “self-host”pnpm build로 만든 dist/를 nginx, S3 + CloudFront, 또는 자체 정적 호스팅 인프라에 올립니다.
# 예: nginx 를 쓰는 경우입니다sudo cp -r dist/* /var/www/saas-app/sudo nginx -s reload
# 예: S3 와 CloudFront 를 쓰는 경우입니다aws s3 sync dist/ s3://saas-app-bucket/aws cloudfront create-invalidation --distribution-id <id> --paths "/*"자동 deploy가 필요하면 GitHub Actions 또는 GitLab CI에서 같은 스크립트를 실행합니다. env는 CI의 secrets에 둡니다.
self-host에서 추가로 챙길 것은 SPA fallback과 캐시 정책입니다. SPA는 클라이언트 라우팅이라 /app/settings 같은 URL로 직접 진입했을 때 nginx가 index.html을 돌려주도록 설정해야 합니다.
location / { try_files $uri $uri/ /index.html;}캐시는 index.html을 no-cache로 두고, JS와 CSS chunk는 long-cache(immutable, 1년)로 두는 편이 안전합니다. Vite가 chunk 파일명에 해시를 넣어 주므로 새 버전을 배포하면 다른 파일명이 만들어집니다.
CSP connect-src
섹션 제목: “CSP connect-src”index.html의 <meta http-equiv="Content-Security-Policy">에 connect-src를 명시합니다. 임의의 외부 URL fetch가 차단됩니다. 보안 기준선 S8에 해당합니다.
<meta http-equiv="Content-Security-Policy" content=" default-src 'self'; connect-src 'self' https://*.supabase.co https://api.stripe.com https://m.stripe.network; script-src 'self' https://js.stripe.com; frame-src https://js.stripe.com https://hooks.stripe.com; img-src 'self' data: https:;">Stripe Checkout이 띄우는 js.stripe.com과 hooks.stripe.com이 script-src와 frame-src에 같이 들어갑니다. Supabase realtime을 쓰면 wss://*.supabase.co를 connect-src에 추가합니다.
측정 도구(Sentry, PostHog, Mixpanel) 같은 것은 본인이 추가합니다. 어떤 도구가 들어갈지는 phase 4에서 결정되고, 이 항목은 fork 할 때 채워집니다.
Stripe 운영 환경 webhook 등록
섹션 제목: “Stripe 운영 환경 webhook 등록”Stripe 운영 환경 모드에서 webhook을 한 번 등록합니다.
- Stripe 대시보드에서 Developers → Webhooks → Add endpoint 로 갑니다.
- Endpoint URL 에
https://<project-ref>.supabase.co/functions/v1/stripe-webhook을 넣습니다. - Events to send 에
checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,invoice.payment_succeeded,invoice.payment_failed를 고릅니다. - 등록 후 표시되는 Signing secret (
whsec_...) 을 복사합니다. - 그 값을 Supabase secret으로 등록합니다.
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...stripe-webhook Edge Function 안의 stripe.webhooks.constructEvent(body, sig, secret)가 이 비밀 값으로 서명을 검증합니다. 값이 일치하지 않으면 모든 이벤트가 거절됩니다. webhook이 도착하는데 DB가 갱신되지 않을 때 가장 자주 발견되는 원인입니다.
stripe trigger 같은 운영 환경 trigger는 자동 실행을 금지합니다(S5). 사용자가 직접 호출할 때만 동작합니다. 로컬에서는 stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook으로 전달만 설정하고, 운영 환경은 실제 결제 이벤트만 받는 편이 안전합니다.
실제 events 목록과 어떤 테이블이 그 이벤트로 갱신되는지는 본인 SaaS의 가격 모델에 따라 다릅니다. 위 여섯 개는 일반적인 SaaS subscription의 기본이고, one-time payment나 usage-based billing이 있는 경우에는 다른 events가 들어갑니다. 이 항목은 fork 할 때 채워집니다.
환경변수 체크리스트
섹션 제목: “환경변수 체크리스트”build-and-package에서 한 번 봤습니다. 호스팅 분기와 함께 다시 봅니다.
- 클라이언트 (
VITE_*) 는 Vercel, Netlify, Cloudflare Pages 호스팅 env에 넣습니다. self-host라면 빌드 환경의 env에 넣습니다. - 서버 (
SUPABASE_SERVICE_ROLE_KEY,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET) 는supabase secrets set으로 Supabase 프로젝트에 넣습니다. 호스팅 env에는 넣지 않습니다.
운영 환경과 staging의 키를 섞지 않는 것이 중요합니다. preview deploy가 운영 환경 Stripe 키를 가리키면 테스트 결제가 실제 결제가 됩니다. Stripe test mode 키(pk_test_..., sk_test_...)와 live mode 키(pk_live_..., sk_live_...)를 환경별로 나눈 다음, env에 어느 키가 들어갔는지 한 번 더 확인하고 운영 환경 배포로 넘어갑니다.
expert tier 끝
섹션 제목: “expert tier 끝”여기까지가 saas-base의 expert tier입니다. intro에서 guardrail 세 개를 짚었고, architecture에서 FSD 6레이어와 어댑터, 행 수준 접근 정책 기준 문서, Stripe 3단 분리를 봤고, verification에서 여섯 관점 review와 보안 S1부터 S8까지를 봤고, build-and-package에서 두 갈래 빌드를 봤고, 여기서 그 산출물을 사용자 브라우저까지 보내는 네 갈래 경로를 나눴습니다.
비개발자 독자를 위한 단계별 안내와 화면 캡처, 호스팅 대시보드 진입 방법은 비개발자용 매뉴얼에 같은 주제를 다른 방식으로 풀어 두었습니다.
다음 섹션은 updates입니다. base가 새 스킬과 갱신된 훅, AI_AUTOMATION.md 변경을 내놓을 때 그것을 안전하게 fork에 가져오는 /update-from-base 채널과, base-owned / user-owned / mixed 영역 분류를 봅니다.