콘텐츠로 이동

배포

pnpm builddist/를 떨궜고 supabase functions deploy로 Edge Function이 Supabase 프로젝트에 올라갔다는 가정 위에서 시작합니다. 여기서는 그 정적 산출물과 함수를 사용자 브라우저까지 보내는 길 네 갈래를 봅니다.

build-and-package까지가 도구가 자동으로 해 주는 영역입니다. 호스팅 선택, custom domain, Stripe production webhook 등록, CSP connect-src 박기는 전부 자동화 밖입니다. production 키와 DNS와 결제 콘솔은 사용자 자격으로 움직여야 하는 자산이라, 도구가 그 자리를 대신 누르면 비밀이 새거나 production에 잘못된 endpoint가 박힙니다.

경로빌드 자동env 주입Edge Function적합한 자리
Vercelgit push 시 자동대시보드 또는 vercel envSupabase 쪽기본 추천, 가장 손이 덜 감
Netlifygit push 시 자동대시보드 또는 netlify env:importSupabase 쪽Vercel 대안
Cloudflare Pagesgit push 시 자동대시보드 또는 wrangler pages secretSupabase 또는 Cloudflare Workersedge 인프라 통합
self-host수동 또는 CI호스팅 환경 직접Supabase 쪽사내 정책상 외부 호스팅 불가

네 경로는 배타적이지 않습니다. staging은 Vercel preview deploy, production은 self-host로 갈라도 됩니다. 한쪽을 끊고 다른 쪽으로 넘기는 마이그레이션도 흔합니다.

호스팅 선택보다 먼저 결정할 자리는 Supabase 프로젝트 분리입니다. production과 staging을 같은 Supabase 프로젝트로 두면 RLS 정책 한 줄 잘못 박힌 실수가 곧장 실사용자 데이터에 닿습니다.

Supabase 조직
├── saas-base-production ← 실 사용자, 실 Stripe live key
└── saas-base-staging ← 개발자 테스트, Stripe test key

각 프로젝트가 자기 SUPABASE_URL, ANON_KEY, SERVICE_ROLE_KEY, STRIPE_*_KEY를 들고 있습니다. 호스팅 환경의 production env와 preview/staging env를 다른 프로젝트로 가리키게 잡습니다.

마이그레이션은 같은 SQL을 양쪽에 적용합니다.

Terminal window
supabase link --project-ref staging-ref
supabase db push # staging에 먼저
# 확인 후
supabase link --project-ref production-ref
supabase db push # production에

UI에서 정책을 만들지 않는 자리(architecture의 RLS 정책 SSOT)가 여기서 같이 박힙니다. 같은 마이그레이션이 두 환경에 같은 모양으로 들어가야 drift가 새지 않습니다.

Terminal window
vercel link # 프로젝트 연결
vercel env pull .env.local # 원격 env를 로컬로
vercel deploy --prod # production deploy

vercel link가 한 번 잡힌 다음에는 git push로 preview deploy가 자동으로 떨어집니다. production deploy는 main 브랜치 push 또는 vercel deploy --prod로 명시 호출합니다.

env 주입은 대시보드의 Settings → Environment Variables에서 하거나, CLI로.

Terminal window
vercel env add VITE_SUPABASE_URL production
vercel env add VITE_SUPABASE_ANON_KEY production
vercel env add VITE_STRIPE_PUBLISHABLE_KEY production

SUPABASE_SERVICE_ROLE_KEYSTRIPE_SECRET_KEY는 여기에 박지 않습니다. 그 둘은 Edge Function 쪽에 supabase secrets set으로 박힙니다. 호스팅 env에 박으면 SPA 번들에 새지는 않더라도 build 시점에 노출 위험이 생깁니다.

custom domain은 대시보드의 Domains 자리. DNS A 또는 CNAME 레코드 한 줄 박는 자리고, Vercel이 SSL 인증서를 자동 발급합니다.

Terminal window
netlify link
netlify env:import .env.production
netlify deploy --prod

netlify env:import.env.production 파일을 통째로 흡수합니다. .env.production은 production 키만 들어있는 파일이고 .gitignore에 있어야 합니다. import 후 그 파일은 지우는 편이 안전합니다.

build 명령은 netlify.toml에 박힙니다.

[build]
command = "pnpm build"
publish = "dist"

custom domain과 SSL은 Vercel과 동일한 흐름입니다.

Terminal window
wrangler pages deploy dist

Cloudflare Pages는 git push로 자동 deploy를 트리거하거나 wrangler pages deploy dist로 수동 push 합니다. 두 갈래 다 잘 동작합니다.

env 주입은 대시보드 또는 wrangler.

Terminal window
wrangler pages secret put VITE_SUPABASE_URL --project-name=saas-base

Cloudflare 환경의 강점은 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 코드가 안 바뀌고 어댑터 한 자리만 갈아끼우는 자리입니다.

pnpm build로 떨군 dist/를 nginx, S3 + CloudFront, 또는 자체 정적 호스팅 인프라에 올립니다.

Terminal window
# 예: 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 파일명에 해시를 박아주므로 새 버전 배포 시 다른 파일명이 떨어집니다.

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.comhooks.stripe.comscript-srcframe-src에 같이 들어갑니다. Supabase realtime을 쓰는 자리는 wss://*.supabase.coconnect-src에 추가합니다.

측정 도구(Sentry, PostHog, Mixpanel) 같은 자리는 본인이 추가합니다. 어떤 도구가 들어갈지는 phase 4에서 결정되고, 이 자리는 fork 시 채워집니다.

Stripe production 모드에서 webhook을 한 번 등록합니다.

  1. Stripe 대시보드 → Developers → Webhooks → Add endpoint
  2. Endpoint URL: https://<project-ref>.supabase.co/functions/v1/stripe-webhook
  3. Events to send: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed
  4. 등록 후 표시되는 Signing secret (whsec_...)을 복사
  5. Supabase secret으로 박기
Terminal window
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...

stripe-webhook Edge Function 안의 stripe.webhooks.constructEvent(body, sig, secret)가 이 비밀로 서명을 검증합니다. 비밀이 mismatch면 모든 이벤트가 거절됩니다. webhook이 도착하는데 DB가 갱신 안 되는 자리에서 가장 자주 잡히는 원인입니다.

stripe trigger 같은 production trigger는 자동 실행 금지(S5). 사용자가 명시 호출할 때만 돕니다. local에서는 stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook으로 forwarding만 잡고, production은 실제 결제 이벤트만 받는 편이 안전합니다.

실 events 목록과 어떤 테이블이 그 이벤트로 갱신되는지는 본인 SaaS의 가격 모델에 따라 다릅니다. 위 6개는 일반적인 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에 박지 않음.

production과 staging의 키를 섞지 않는 자리가 중요합니다. preview deploy가 production Stripe 키를 가리키면 테스트 결제가 실제 결제로 떨어집니다. Stripe test mode 키(pk_test_..., sk_test_...)와 live mode 키(pk_live_..., sk_live_...)를 환경별로 가른 다음, env에 어느 키가 박혔는지 한 번 더 확인하고 production deploy를 진입합니다.

여기까지가 saas-base의 expert tier입니다. intro에서 guardrail 세 개를 짚고, architecture에서 FSD 6레이어와 어댑터와 RLS 정책 SSOT와 Stripe 3단 분리를 봤고, verification에서 6차원 review와 보안 S1~S8을 봤고, build-and-package에서 두 트랙 빌드를 봤고, 여기서 그 산출물을 사용자 브라우저까지 보내는 길 네 갈래를 갈랐습니다.

비개발자 청중을 위한 단계별 안내, 화면 캡처, 호스팅 대시보드 진입은 비개발자용 매뉴얼에 같은 주제를 다른 호흡으로 풀어두었습니다.

다음 섹션은 updates입니다. base가 새 스킬, 갱신된 훅, AI_AUTOMATION.md 변경을 내놓을 때 그것을 안전하게 fork에 가져오는 /update-from-base 채널과, base-owned / user-owned / mixed 영역 분류를 봅니다.