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 를 사용한 서명 검증은 필수입니다.
서명 검증을 건너뛰면 공격자가 이벤트를 위조하고 결제 상태를 조작할 수 있으므로 항상 검증하세요.