FlipSchedule
Problem ― 何を解決したかったか
既存の出欠調整ツール(調整さんなど)は機能が限定的で、以下の課題を抱えていた:
- 複数イベントの一元管理ができない
- パスワード保護やユーザー認証が不十分
- 主催者の管理機能(編集・削除)が貧弱
- モダンなUI/UXを備えたサービスが存在しない
Solution ― どう実装したか
Next.js 16のServer Actions + Prisma + Stripeを組み合わせ、フルスタックSaaSプラットフォームを構築。 APIルートを一切書かずにDB操作を完結させ、開発速度を大幅に向上させた。
Architecture ― システム構成図
フロントエンドからDB層まで、一気通貫のフルスタック構成。 Server ActionsによりAPIルートを排除し、型安全性を端から端まで保証している。
FlipSchedule — Full-Stack Architecture
Tech Decision ― Next.js 16の採用理由
単に「最新だから」ではなく、Server ActionsによってAPIルートを書かずにDB操作が可能になり、 以下のメリットが得られたことが採用理由:
Deep Dive ― Server Actions
イベント作成のServer Action
クライアントから直接サーバー関数を呼び出し、Prismaで型安全にDB操作。 セッション検証も同一関数内で完結するため、 セキュリティバグの入り込む余地が少ない。
1class=class="syntax-string">"syntax-comment">// Server Action: イベント作成(APIルート不要)2class="syntax-string">"use server";34export async function createEvent(formData: FormData) {5 const session = await auth();6 if (!session?.user) throw new Error(class="syntax-string">"Unauthorized");78 const title = formData.get(class="syntax-string">"title") as string;9 const dates = formData.getAll(class="syntax-string">"dates") as string[];1011 class=class="syntax-string">"syntax-comment">// PrismaでタイプセーフなDB操作12 const event = await prisma.event.create({13 data: {14 title,15 ownerId: session.user.id,16 expiresAt: calculateExpiry(dates),17 candidates: {18 create: dates.map((date) => ({19 dateTime: new Date(date),20 })),21 },22 },23 include: { candidates: true },24 });2526 revalidatePath(class="syntax-string">"/dashboard");27 return { success: true, eventId: event.id };28}
Deep Dive ― Stripe決済
Webhookでの非同期処理
Checkout Session作成だけでなく、Webhookによる非同期の決済結果処理を実装。 決済完了時にDBのサブスクリプション状態を更新する仕組みにより、 信頼性の高い課金システムを実現した。
決済フロー
1class=class="syntax-string">"syntax-comment">// Stripe Webhook: 決済完了後の非同期処理2export async function POST(req: NextRequest) {3 const body = await req.text();4 const sig = req.headers.get(class="syntax-string">"stripe-signature")!;56 class=class="syntax-string">"syntax-comment">// 署名検証(セキュリティの要)7 const event = stripe.webhooks.constructEvent(8 body, sig, process.env.STRIPE_WEBHOOK_SECRET!9 );1011 if (event.type === class="syntax-string">"checkout.session.completed") {12 const session = event.data.object;1314 class=class="syntax-string">"syntax-comment">// DBのサブスクリプション状態を更新15 await prisma.user.update({16 where: { id: session.metadata.userId },17 data: {18 plan: class="syntax-string">"premium",19 stripeCustomerId: session.customer,20 subscriptionId: session.subscription,21 },22 });23 }2425 return new Response(class="syntax-string">"OK", { status: 200 });26}
Deep Dive ― 認証 (Auth.js v5)
NextAuth (Auth.js v5) を使用し、Google / LINE / Microsoftの3つの OAuthプロバイダーを統合。JWTベースのセッション管理により、 DBへのセッション問い合わせを最小化しつつ、セキュアな認証を実現。
セッション管理
- JWTストラテジーを採用
- Session callbackでカスタムクレーム付与
- Server ComponentからgetServerSession()で取得
セキュリティ
- CSRF保護(自動)
- Middlewareによるルート保護
- アカウントリンク(複数プロバイダー)
AI-Augmented Development
Cursor (Claude) を活用して開発を加速。ただし、AIに「任せた部分」と 「人間が設計・判断した部分」を明確に区別している。
AI (Cursor) が担当
UIコンポーネントのボイラープレート生成、CSSスタイリング、 型定義の雛形作成、テスト用のモックデータ生成
人間が担当
サービスの仕様策定、DBスキーマ設計、 Stripe Webhook署名検証のセキュリティレビュー、 NextAuthのセッション戦略決定、本番環境のデバッグ
Challenges & Learnings
NextAuthのセッション管理にJWTとDBの統合で苦戦
Session callbackでDBからユーザー情報を取得し、JWTにカスタムクレームとしてプラン情報を付与する設計に落ち着いた。
Stripe Webhookの署名検証がローカル開発で動作しない
Stripe CLIのwebhook forwardingを使用し、localhost:3000/api/webhookにイベントを転送する開発環境を構築。
Server Actionsのエラーハンドリングパターン
try-catchで包むだけでなく、Zodによるバリデーションを最初に行い、型安全なエラーレスポンスを返す設計を採用。