본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 04. 29. 09:56

Supabase × Stripe — Edge Functions 를 사용한 구독 결제 구현

요약

본 문서는 Supabase와 Stripe를 결합하여 안정적인 구독 결제 시스템을 구축하는 방법을 설명합니다. 핵심은 Supabase Edge Functions(EF)를 활용하여 Stripe Checkout 세션을 생성하고, Stripe Webhook을 통해 발생하는 이벤트를 수신하여 데이터베이스(DB)의 구독 상태를 실시간으로 업데이트하는 것입니다. 이를 통해 프론트엔드(Flutter)는 안전하게 결제 흐름을 시작하고, 백엔드는 모든 상태 변화를 신뢰할 수 있는 방식으로 관리할 수 있습니다.

핵심 포인트

  • Supabase Edge Functions를 사용하여 Stripe Checkout 세션을 생성하여 클라이언트 측의 보안 위험을 최소화합니다.
  • Stripe Webhook을 Supabase EF로 받아 구독 상태 변경(생성, 업데이트, 삭제)을 감지하고 DB에 반영합니다.
  • DB는 RLS(Row Level Security)로 보호되며, Flutter 앱은 이 테이블에서 최신 구독 상태를 읽어 사용자 인터페이스를 구성합니다.
  • 전체 아키텍처 흐름: Flutter → EF (세션 생성) → Stripe Checkout → Webhook → EF (DB 업데이트)
  • Stripe Customer ID 관리를 위해 Supabase DB의 `profiles` 테이블을 활용하여 고객 정보를 통합 관리할 수 있습니다.

Supabase × Stripe — Edge Functions 를 사용한 구독 결제 시스템 구축

Stripe 와 Supabase Edge Functions 를 사용하여 월별 구독 시스템을 구축합니다.

아키텍처 개요
Flutter → Supabase EF (create-checkout) → Stripe Checkout
Stripe Webhook → Supabase EF (stripe-webhook) → DB 업데이트
Flutter → Supabase DB (구독 상태 읽기)

Checkout 세션 생성
// supabase/functions/create-checkout/index.ts
import Stripe from "npm:stripe";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);

Deno.serve(async(req) => {
const { userId, priceId } = await req.json();

// 기존 Stripe Customer ID 조회 (없으면 생성)
const { data: profile } = await supabase.from("profiles").select("stripe_customer_id, email").eq("id", userId).single();
let customerId = profile.stripe_customer_id;
if (!customerId) {
const customer = await stripe.customers.create({ email: profile.email });
customerId = customer.id;
await supabase.from("profiles").update({ stripe_customer_id: customerId }).eq("id", userId);
}

const session = await stripe.checkout.sessions.create({
customer: customerId,
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
success_url: ${Deno.env.get("APP_URL")}/success?session_id={CHECKOUT_SESSION_ID},
cancel_url: ${Deno.env.get("APP_URL")}/pricing,
metadata: { user_id: userId },
});

return new Response(JSON.stringify({ url: session.url }));
});

Webhook 을 통한 DB 업데이트
// supabase/functions/stripe-webhook/index.ts
Deno.serve(async(req) => {
const sig = req.headers.get("stripe-signature")!;
const body = await req.text();
const event = stripe.webhooks.constructEvent(body, sig, Deno.env.get("STRIPE_WEBHOOK_SECRET")!);

switch (event.type) {
case "customer.subscription.created":
case "customer.subscription.updated": {
const sub = event.data.object as Stripe.Subscription;
const userId = sub.metadata.user_id;
await supabase.from("subscriptions").upsert({
user_id: userId,
stripe_subscription_id: sub.id,
status: sub.status, // active / past_due / canceled
current_period_end: new Date(sub.current_period_end * 1000).toISOString(),
plan: sub.items.data[0].price.lookup_key,
});
break;
}
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription;
await supabase.from("subscriptions").update({ status: "canceled" }).eq("stripe_subscription_id", sub.id);
break;
}
}

return new Response("ok");
});

Flutter 에서 Checkout 열기
Future<void> openCheckout(String priceId) async {
final res = await supabase.functions.invoke('create-checkout', body: {
'userId': supabase.auth.currentUser!.id,
'priceId': priceId,
});

final url = (res.data as Map)['url'] as String;
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
}

요약
Checkout → EF 가 Stripe 세션을 생성 → Flutter 가 URL 을 엽니다.
Webhook → Stripe → EF → subscriptions 테이블 업데이트
Status → Flutter 는 RLS 로 보호된 subscriptions 테이블을 직접 읽습니다.
Security → STRIPE_WEBHOOK_SECRET 를 사용한 서명 검증은 필수입니다.
서명 검증을 건너뛰면 공격자가 이벤트를 위조하고 결제 상태를 조작할 수 있으므로 항상 검증하세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
10

댓글

0