はじめに
個人開発で有料サービスを作るとき、こんな悩みはありませんか?
- 「会員登録画面を作るの面倒だな...」
- 「せっかく購入ボタンを押してくれたのに、会員登録で離脱されそう」
- 「認証周りのセキュリティ、ちゃんとできてるか不安」
従来の購入フローは 会員登録 → ログイン → 購入 という3ステップが当たり前でした。でも、これって本当に必要でしょうか?
この記事では、「決済と同時にアカウント作成」 という発想で、ユーザー体験を大幅に改善する設計パターンを紹介します。
この記事で得られること
- 会員登録画面を作らずに済む設計パターン
- Stripe Checkoutを活用した具体的なフロー
- 次回ログインの解決策(マジックリンク)
- 実装時の注意点とエッジケース
従来のフローの問題点
よくある購入フローを図にすると、こうなります。
従来のフロー(3ステップ)
[購入ボタン] → [会員登録画面] → [ログイン] → [決済画面] → [完了]
↓
ここで離脱 😢
😓 何が問題か?
-
離脱ポイントが多い
- 会員登録フォームを見た瞬間に「面倒くさい」と思われる
- パスワードを考えるのが億劫
-
実装コストが高い
- 会員登録画面
- メール認証
- パスワードリセット
- ログイン画面
- これ全部作るの?
-
セキュリティの責任
- パスワードのハッシュ化、ちゃんとできてる?
- セッション管理、大丈夫?
発想の転換:「決済 = 会員登録」
ここで逆転の発想です。
「購入する人」は「会員になる人」と同じ ですよね?
だったら、決済のタイミングでアカウントを作ればいい。
新しいフロー(1ステップ)
[購入ボタン] → [Stripe決済画面] → [完了・自動ログイン]
↓
メールアドレスはここで入力
(Stripeが取得してくれる)
✨ このフローのメリット
| 項目 | 従来 | 新フロー |
|---|---|---|
| 会員登録画面 | 必要 | 不要 |
| パスワード設定 | 必須 | 後から任意 |
| 離脱ポイント | 多い | 最小限 |
| 実装コスト | 高い | 低い |
具体的なフロー設計
では、具体的にどう実装するか見ていきましょう。
📝 購入フロー(初回)
1. ユーザー:「購入ボタン」を押す
2. サイト:住所入力画面を表示(※任意。サービスで使う場合)
3. サイト:Stripe Checkout セッションを作成
4. ユーザー:Stripe画面でメールアドレス+カード情報を入力
5. Stripe:決済完了 → Webhook でサイトに通知
6. サイト(Webhook処理):
- メールアドレスでユーザーを検索
- いなければ新規作成
- セッション発行
7. ユーザー:完了画面(ログイン済み状態)
🔧 実装のポイント
1. Stripe Checkoutでメールアドレスを取得
Stripe Checkoutを使うと、決済画面でメールアドレスを入力させることができます。このメールアドレスがそのまま「ユーザーID」になります。
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [
{
price: 'price_xxxxx', // Stripeで作成した価格ID
quantity: 1,
},
],
success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://example.com/cancel',
// ↓ これが重要!メールアドレスを取得できる
customer_email: undefined, // undefinedにすると、ユーザーに入力させる
});
2. Webhookでユーザー自動作成
決済完了時にStripeからWebhookが飛んできます。ここでユーザーを作成します。
// Stripe Webhook: checkout.session.completed
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const email = session.customer_details?.email;
if (!email) {
throw new Error('Email not found');
}
// 既存ユーザーを検索
let user = await db.selectFrom('users')
.where('email', '=', email)
.selectAll()
.executeTakeFirst();
// いなければ新規作成
if (!user) {
const result = await db.insertInto('users')
.values({
email,
plan: 1, // プレミアムプラン
plan_purchased_at: new Date().toISOString(),
})
.returning(['id', 'email'])
.executeTakeFirst();
user = result;
} else {
// 既存ユーザーならプランをアップグレード
await db.updateTable('users')
.set({
plan: 1,
plan_purchased_at: new Date().toISOString(),
})
.where('id', '=', user.id)
.execute();
}
// 購入履歴を保存
await db.insertInto('purchases')
.values({
user_id: user.id,
stripe_session_id: session.id,
amount: session.amount_total,
status: 'completed',
})
.execute();
return user;
}
3. 完了画面でセッション発行
Webhookの処理が終わったら、完了画面でユーザーをログイン状態にします。
// /success?session_id=xxx のページ
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const sessionId = url.searchParams.get('session_id');
// Stripeからセッション情報を取得
const session = await stripe.checkout.sessions.retrieve(sessionId);
const email = session.customer_details?.email;
// ユーザーを取得してセッション発行
const user = await db.selectFrom('users')
.where('email', '=', email)
.selectAll()
.executeTakeFirst();
// Cookieにセッショントークンをセット
return redirect('/dashboard', {
headers: {
'Set-Cookie': await createSession(user.id),
},
});
}
次回ログインはどうする?
ここで疑問が出てきます。
「パスワードを設定してないけど、次回どうやってログインするの?」
🔐 解決策:マジックリンク
マジックリンク とは、メールに送られたURLをクリックするだけでログインできる仕組みです。
次回ログインフロー
1. ユーザー:「ログイン」ボタンを押す
2. サイト:メールアドレス入力画面を表示
3. ユーザー:メールアドレスを入力
4. サイト:ログイン用URLをメール送信
5. ユーザー:メールのURLをクリック
6. サイト:トークン検証 → ログイン完了
パスワードは後から設定可能に
マジックリンクだけだと毎回メール確認が必要で面倒、という人もいます。そこで、パスワードは後から任意で設定できる ようにします。
マイページ
├── プロフィール編集
└── パスワード設定 ← これを用意
└── 「パスワードを設定すると、次回からメール+パスワードでログインできます」
エッジケースへの対応
実装時に考慮すべきケースをまとめます。
ケース1: 同じメールで再度購入しようとした
// Stripe Checkout前にチェック
const existingUser = await db.selectFrom('users')
.where('email', '=', inputEmail)
.selectAll()
.executeTakeFirst();
if (existingUser && existingUser.plan === 1) {
// すでにプレミアム会員
return json({ error: 'すでにプレミアム会員です。ログインしてください。' });
}
ケース2: 購入済みユーザーが未ログインで購入ボタンを押した
これはWebhookで吸収できます。既存ユーザーならプラン情報を更新するだけ。
ケース3: パスワード忘れ(設定済みの場合)
マジックリンクでログイン → マイページでパスワード再設定、という導線を用意。
DB設計(参考)
-- ユーザーテーブル
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT, -- NULL許容(後から設定)
plan INTEGER DEFAULT 0, -- 0:無料, 1:プレミアム
plan_purchased_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- 購入履歴
CREATE TABLE purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
stripe_session_id TEXT NOT NULL,
amount INTEGER,
status TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- マジックリンク
CREATE TABLE magic_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at TEXT NOT NULL,
used_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
まとめ
この記事では、「決済と同時にアカウント作成」 というパターンを紹介しました。
✅ ポイントおさらい
- 会員登録画面は不要 - Stripe Checkoutでメールアドレスを取得
- パスワードも不要 - 後から任意で設定可能に
- 離脱ポイント最小化 - 購入ボタン → 決済 → 完了 の1本道
- 次回ログイン - マジックリンクで解決
- セキュリティ - 認証の複雑な部分はClerkやSupabaseに任せるのも手
💭 最後に
個人開発では「作らなくていいものは作らない」が鉄則です。
会員登録画面、ログイン画面、パスワードリセット...これらを全部自前で作る必要は本当にあるでしょうか?
「買ったらすぐ使える」 というシンプルな体験が、ユーザーにとっても開発者にとっても幸せな選択かもしれません。
最後まで読んでいただきありがとうございました!
質問やフィードバックがあれば、ぜひコメントでお聞かせください 🙌