Case Study #1

FlipSchedule

Project
FlipSchedule
Type
SaaS — 出欠管理プラットフォーム
Stack
Next.js 16 / Prisma / Stripe / Auth.js
Status
Production
Period
2025 Q3 – Present

Problem ― 何を解決したかったか

既存の出欠調整ツール(調整さんなど)は機能が限定的で、以下の課題を抱えていた:

  • 複数イベントの一元管理ができない
  • パスワード保護やユーザー認証が不十分
  • 主催者の管理機能(編集・削除)が貧弱
  • モダンなUI/UXを備えたサービスが存在しない

Solution ― どう実装したか

Next.js 16のServer Actions + Prisma + Stripeを組み合わせ、フルスタックSaaSプラットフォームを構築。 APIルートを一切書かずにDB操作を完結させ、開発速度を大幅に向上させた。

Architecture ― システム構成図

フロントエンドからDB層まで、一気通貫のフルスタック構成。 Server ActionsによりAPIルートを排除し、型安全性を端から端まで保証している。

System Architecture

FlipSchedule — Full-Stack Architecture

👤 Client (Browser)
HTTPS
Next.js 16 (App Router)
React RSC
Server Actions
Middleware
Server Actions (no API routes)
NextAuth / Auth.js
Prisma ORM
Type-safe Query
PostgreSQL / SQLite
External Services
Stripe Checkout
Stripe Webhook
Vercel

Tech Decision ― Next.js 16の採用理由

単に「最新だから」ではなく、Server ActionsによってAPIルートを書かずにDB操作が可能になり、 以下のメリットが得られたことが採用理由:

0
API Routes Required
Type-Safe
End-to-End
RSC
React Server Components

Deep Dive ― Server Actions

イベント作成のServer Action

クライアントから直接サーバー関数を呼び出し、Prismaで型安全にDB操作。 セッション検証も同一関数内で完結するため、 セキュリティバグの入り込む余地が少ない。

lib/actions.ts
TypeScript
1class=class="syntax-string">"syntax-comment">// Server Action: イベント作成(APIルート不要)
2class="syntax-string">"use server";
3
4export async function createEvent(formData: FormData) {
5 const session = await auth();
6 if (!session?.user) throw new Error(class="syntax-string">"Unauthorized");
7
8 const title = formData.get(class="syntax-string">"title") as string;
9 const dates = formData.getAll(class="syntax-string">"dates") as string[];
10
11 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 });
25
26 revalidatePath(class="syntax-string">"/dashboard");
27 return { success: true, eventId: event.id };
28}

Deep Dive ― Stripe決済

Webhookでの非同期処理

Checkout Session作成だけでなく、Webhookによる非同期の決済結果処理を実装。 決済完了時にDBのサブスクリプション状態を更新する仕組みにより、 信頼性の高い課金システムを実現した。

決済フロー

1. Client
2. Checkout Session作成
3. Stripe決済画面
4. Webhook → DB更新
app/api/webhook/route.ts
TypeScript
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")!;
5
6 class=class="syntax-string">"syntax-comment">// 署名検証(セキュリティの要)
7 const event = stripe.webhooks.constructEvent(
8 body, sig, process.env.STRIPE_WEBHOOK_SECRET!
9 );
10
11 if (event.type === class="syntax-string">"checkout.session.completed") {
12 const session = event.data.object;
13
14 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 }
24
25 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によるバリデーションを最初に行い、型安全なエラーレスポンスを返す設計を採用。