はじめに
個人開発のWebアプリにサブスクリプション機能を実装しました。
Stripe の代替として Lemon Squeezy を使った理由と実装を解説します。
Lemon Squeezy を選んだ理由
- MoR(Merchant of Record): 消費税・VAT処理を代行してくれる
- 日本円対応: 円建て価格設定が可能
- API が簡単: Stripe より学習コストが低い
- ダッシュボードが使いやすい
チェックアウトセッションの作成
// api/create-checkout-session.js (Vercel Serverless Function)
export default async function handler(req, res) {
const { userId, email, plan } = req.body
const variantId = plan === 'yearly'
? process.env.LS_VARIANT_YEARLY
: process.env.LS_VARIANT_MONTHLY
const response = await fetch('https://api.lemonsqueezy.com/v1/checkouts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LEMONSQUEEZY_API_KEY}`,
'Content-Type': 'application/vnd.api+json',
},
body: JSON.stringify({
data: {
type: 'checkouts',
attributes: {
checkout_data: { email, custom: { user_id: userId } },
},
relationships: {
store: { data: { type: 'stores', id: process.env.LS_STORE_ID } },
variant: { data: { type: 'variants', id: variantId } },
},
},
}),
})
const { data } = await response.json()
res.json({ url: data.attributes.url })
}
Webhook でサブスクリプション状態を管理
// api/lemonsqueezy-webhook.js
export default async function handler(req, res) {
const event = req.headers['x-event-name']
const payload = req.body
if (event === 'subscription_created' || event === 'subscription_updated') {
const userId = payload.meta.custom_data?.user_id
const status = payload.data.attributes.status
await supabase.from('subscriptions').upsert({
user_id: userId,
plan: status === 'active' ? 'pro' : 'free',
ls_subscription_id: payload.data.id,
})
}
res.status(200).json({ ok: true })
}
フロントエンドでのプラン分岐
// Pro機能へのアクセス制御
const isPro = subscription?.plan === 'pro'
{isPro ? (
<AlertSettings />
) : (
<div>
<p>Pro プランで利用できます</p>
<UpgradeButton />
</div>
)}
まとめ
Lemon Squeezy を使えば個人開発者でも簡単にサブスクリプション機能を実装できます。
MoR として税務処理を代行してくれるので、海外ユーザーへの販売も安心です。