セットアップガイド

StartPack - 30分で始めるSaaS開発

# 概要

StartPackは、TypeScript + Next.jsをベースに、認証、決済、お問い合わせフォームが事前設定された完全なSaaS基盤を提供します。

主要機能

  • ユーザー認証(Neon Auth または Supabase Authから選択)
  • Stripeによるサブスクリプション決済
  • Resendによるお問い合わせメール送信
  • PostgreSQL(Neon / Supabase から選択、Prisma ORM使用)
  • Tailwind CSS + shadcn/uiコンポーネント

ディレクトリ構造

startpack/
├── app/                    # Next.js App Router
│   ├── api/               # APIルート
│   ├── auth/              # 認証ページ
│   ├── billing/           # サブスクリプション管理
│   ├── dashboard/         # ユーザーダッシュボード
│   ├── contact/           # お問い合わせフォーム
│   ├── handler/           # Stack Auth設定
│   └── legal/             # 利用規約等
├── components/            # Reactコンポーネント
├── lib/                   # ユーティリティ関数
├── prisma/                # データベーススキーマ
├── config/                # アプリ設定
├── public/                # 静的ファイル
└── styles/                # グローバルスタイル

# チュートリアル動画

StartPackの機能紹介から実際のセットアップ手順まで、動画でわかりやすく解説しています。

# クイックスタート

アプリケーションをローカル環境で起動する手順です。

# unzip
unzip startpack.zip
cd startpack

# 初期化(必ず先に実行)
npm run startpack:init
# プロンプトで番号を入力してプロバイダを選択します
# 1) Neon(StackAuth) / 2) Supabase Auth
# 選択したプロバイダ以外の依存とコードは自動で削除されます

# 依存をインストール(init の後)
npm install

理由: 初期化スクリプトが package.json の依存関係やファイル構成をプロバイダに合わせて変更するため、先に初期化その後インストール の順にしてください。

共通準備

  1. 環境変数ファイルをコピー(どちらか一方を選択)
    # Neon を使う場合
    cp .env.neon.example .env
    
    # Supabase を使う場合
    cp .env.supabase.example .env
  2. 依存関係をインストール
    npm install
  3. .env を開き、下記「環境変数」を参考に必要項目を埋めます

4. Neonデータベースの接続情報を設定

  1. neon.tech でプロジェクトを作成(AWSリージョンはシンガポールがおすすめ)
  2. ダッシュボードの 「Connect to your database」 セクションを見つける
  3. 「Connect」 ボタンをクリック
  4. 表示された 「Connection string」 をコピー(パスワード含む)
  5. .envDATABASE_URL に貼り付け
# .env DATABASE_URL="postgresql://user:password@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"

5. データベースを初期化

npx prisma migrate dev

Prisma CLI は .env を自動読み込みします。DATABASE_URL.env に必ず設定してください。

6. 開発サーバーを起動

npm run dev

認証、決済、メールなどを利用する場合は、下記の環境変数.env をすべて埋めてから実行してください。

http://localhost:3002 でアプリケーションにアクセスできます

# 環境変数

プロバイダ別のテンプレートを切替表示できます。

# データベース(Neon)
DATABASE_URL=postgresql://user:password@host/database?sslmode=require

# 認証(Neon Auth / StackAuth)
NEXT_PUBLIC_STACK_PROJECT_ID=your-project-id
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=your-publishable-key
STACK_SECRET_SERVER_KEY=your-secret-server-key

# 決済(Stripe)
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRICE_ID=price_...
STRIPE_WEBHOOK_SECRET=whsec_...

# メール(Resend)
RESEND_API_KEY=re_...
CONTACT_EMAIL=you@example.com
# RESEND_FROM=support@yourdomain.com
# RESEND_DOMAIN=yourdomain.com

# レート制限(任意)
CONTACT_RATE_LIMIT_WINDOW_MS=60000
CONTACT_RATE_LIMIT_MAX=5

# 認証設定

StartPack は Neon Auth(StackAuth)または Supabase Auth を選択できます(npm run startpack:init)。未選択のコードと依存は自動削除されます。

Neon Auth設定

Neon AuthはNeonデータベースに統合された認証システムで、内部でStackAuthの強力な認証機能を利用しています。

セットアップ手順

  1. Neonダッシュボードで Auth を有効化
  2. Configuration タブを開き、表示される環境変数をコピーして .env に設定
  3. Settings → Project ID から NEON_PROJECT_ID をコピーして .env に設定
  4. 右上のアカウントアイコン → Account settings → Personal API keys で API トークンを作成し、NEON_API_TOKEN として .env に設定

必要な環境変数(Configurationからコピー)

変数説明
NEXT_PUBLIC_STACK_PROJECT_IDStackAuth プロジェクトID
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY公開クライアントキー
STACK_SECRET_SERVER_KEYサーバーシークレットキー
NEON_PROJECT_IDNeonプロジェクト ID(退会機能で必須)
NEON_API_TOKENNeon API トークン(退会機能で必須)

📖 詳細なセットアップ手順:Neon Auth Next.jsクイックスタート

Neon Auth はプロジェクトごとに1構成

同一の Neon プロジェクト内に複数の Neon Auth 構成は設定できません。開発環境と本番環境で認証を分離したい場合は、 環境ごとに 別々の Neon プロジェクト を作成してください。

推奨手順
  1. Neon コンソールで 開発用プロジェクト(例: startpack-dev) を作成し、Auth を有効化。Configuration の値を .env に設定。
  2. 別途 本番用プロジェクト(例: startpack-prod) を作成し、同様に Auth を有効化。本番環境の環境変数に設定。
  3. 各環境で使用する DATABASE_URL は、それぞれのプロジェクトの接続文字列を使用します。
  4. ユーザーや users_sync テーブルは環境ごとに独立します(開発と本番で混ざりません)。

既に単一プロジェクトで運用している場合は、もう一つプロジェクトを新規作成し、Auth を有効化後にアプリ側の環境変数(StackAuth と DATABASE_URL)を切り替えてください。

🔧 設定をカスタマイズしたい場合: Neon Auth の設定ページにあるClaim projectからプロジェクトを Stack Auth に移管すると、 Stack Auth ダッシュボード上で詳細な設定や管理が可能になります。

# 決済連携

Stripe設定

開発環境でのテスト: Stripe は必ず テストモード(サンドボックス) を使用してください。 APIキー、商品/価格、Webhook いずれも テストモード で取得・作成したものを使います。

  • キーの接頭辞: sk_test_ / pk_test_
  • Price ID はテストモードで作成したもの(例: price_...
  • Webhook シークレットもテストモード側で取得

1. APIキー

  1. Stripeダッシュボードにログイン
  2. (開発環境)右上の テストモード(サンドボックス)をON
  3. 開発者 → APIキー に移動
  4. 公開可能キー(NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)とシークレットキー(STRIPE_SECRET_KEY)の両方をコピー

.env に設定:

STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

2. サブスクリプション商品の作成

開発環境では テストモード 側で商品/価格を作成してください(本番とは別管理です)。

  1. 商品 → 商品を追加 に移動
  2. 商品名と価格を設定
  3. 課金タイプは「継続」、請求周期は「毎月」を選択
  4. Price ID(形式: price_xxx)をコピー

.env に設定:

STRIPE_PRICE_ID=price_...

3. Webhook設定

エンドポイントURL:

https://yourdomain.com/api/stripe/webhook

必要なイベント(StartPackのサブスク実装):

  • checkout.session.completed(初回決済完了 → サブスクリプション保存)
  • customer.subscription.updated(キャンセル予約・期間更新 等の反映)
  • customer.subscription.deleted(サブスクリプション終了)
  • invoice.payment_succeeded(月額課金の成功を反映)
  • invoice.payment_failed(課金失敗を反映)

なお、必要に応じて受信するWebhookイベントや処理内容は調整してください(例: トライアル開始/終了、支払い要承認など)。

Stripe CLIでのローカル開発

インストール手順はStripe CLI公式ドキュメントを参照してください。

事前に Stripe ダッシュボードへ テスト(サンドボックス)環境でログインしてから、下記の stripe login を実行してください(ブラウザが開いて認証されます)。

# ログイン
stripe login

# webhookをlocalhostに転送
stripe listen --forward-to localhost:3002/api/stripe/webhook

# 表示されたwebhookシークレットをコピー

.env に設定:

STRIPE_WEBHOOK_SECRET=whsec_...

# 有料プラン加入中の判定

推奨ロジック

  • プラン加入中(有効扱い): active / trialing / past_due
  • 加入なし(利用不可扱い): canceled / incomplete / unpaid
  • キャンセル予約中: cancelAtPeriodEnd === true(期日までは利用可能)

クライアント側(React)の例

// 任意のコンポーネント内
import { useEffect, useState } from 'react'

type SubStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete' | 'unpaid'

function usePaidPlan() {
  const [loading, setLoading] = useState(true)
  const [isPaid, setIsPaid] = useState(false)
  const [cancelAtPeriodEnd, setCancelAtPeriodEnd] = useState(false)

  useEffect(() => {
    let mounted = true
    ;(async () => {
      try {
        const res = await fetch('/api/stripe/subscription', { cache: 'no-store' })
        if (!mounted) return
        if (!res.ok) { setIsPaid(false); setLoading(false); return }
        const data = await res.json()
        const status: SubStatus | null = data?.subscription?.status ?? null
        const cancelFlag: boolean = Boolean(data?.subscription?.cancelAtPeriodEnd)
        const paid = status === 'active' || status === 'trialing' || status === 'past_due'
        setIsPaid(Boolean(status && paid))
        setCancelAtPeriodEnd(cancelFlag)
      } catch { setIsPaid(false) } finally { setLoading(false) }
    })()
    return () => { mounted = false }
  }, [])
  return { loading, isPaid, cancelAtPeriodEnd }
}

// 使い方例
export default function PremiumGate({ children }: { children: React.ReactNode }) {
  const { loading, isPaid, cancelAtPeriodEnd } = usePaidPlan()
  if (loading) return <div className='text-gray-400 text-sm'>読み込み中...</div>
  if (!isPaid) return <div className='text-gray-400 text-sm'>プラン登録が必要です</div>
  return <div>{children}{cancelAtPeriodEnd && <div className='mt-2 text-xs text-gray-500'>キャンセル予定(期日まではご利用可)</div>}</div>
}

サーバー側(Route / Server Component)の例

// 例) /app/premium/page.tsx (Server Component)
import 'server-only'

async function fetchSubscription() {
  const res = await fetch('/api/stripe/subscription', { cache: 'no-store' })
  if (!res.ok) return null
  return res.json()
}

export default async function PremiumPage() {
  const data = await fetchSubscription()
  const status = data?.subscription?.status as ('active'|'trialing'|'past_due'|'canceled'|'incomplete'|'unpaid') | undefined
  const isPaid = status === 'active' || status === 'trialing' || status === 'past_due'
  if (!isPaid) {
    return <div className='p-6 text-sm text-gray-400'>プラン登録が必要です</div>
  }
  return <div className='p-6'>有料ページの内容</div>
}

# カスタマーポータル

Stripeのカスタマーポータルを有効化して、ユーザーが自分で支払い方法や請求履歴、キャンセル/再開を管理できるようにします。

Stripeダッシュボードで有効化

  1. Stripeダッシュボード設定Billingカスタマーポータル
  2. カスタマーポータルのリンクを有効にする をオン

# メール設定

Resendセットアップ

  1. resend.com でサインアップ
  2. ダッシュボードでAPIキーを作成
  3. RESEND_API_KEY に追加
  4. お問い合わせフォームの受信用に CONTACT_EMAIL を設定

ドメイン検証(本番環境)

  1. Resendダッシュボードでドメインを追加
  2. DNSレコードを設定(SPF、DKIM)
  3. ドメイン所有権を確認
  4. .envの RESEND_DOMAIN を更新

⚠️ 開発環境の制限: ドメイン検証なしでは、アカウント所有者のメールアドレスにのみメールを送信できます。

さらに、未検証ドメインでは送信元は onboarding@resend.dev のみ許可されます(noreply@resend.dev などは不可)。

ローカルでは CONTACT_EMAIL を Resend アカウントのメール(または許可された受信先)に設定してください。

Neon(Stack Auth)の認証メールをカスタマイズするには:
Neon コンソール → AuthProject ownershipClaim project を実行すると、 Stack Auth 側でメールテンプレート(確認メール、パスワードリセット等)を編集できます。

# お問い合わせフォームのレートリミット

お問い合わせフォームには、スパム防止のためのレートリミット機能が搭載されています。 同一IPアドレスから短時間に大量のリクエストが送信された場合、一時的にアクセスを制限します。

デフォルト設定

  • 制限時間: 60秒間
  • 最大送信回数: 5回
  • 制限対象: IPアドレス単位

つまり、同じIPアドレスから60秒間に6回目以降のお問い合わせは一時的に拒否されます。

設定のカスタマイズ

環境変数で制限を調整できます:

  • CONTACT_RATE_LIMIT_WINDOW_MS: 制限時間(ミリ秒)。デフォルト 60000(60秒)
  • CONTACT_RATE_LIMIT_MAX: 最大送信回数。デフォルト 5

制限時の表示: 制限に達すると「リクエストが多すぎます。しばらく時間をおいて再度お試しください。」というメッセージが表示されます。

# お問い合わせメールのカスタマイズ

お問い合わせフォームのメール送信は /app/api/contact/route.ts で実装されています。 テキスト形式のメールで、件名・本文・送信元を自由にカスタマイズできます。

現在の設定

  • メール形式: テキスト形式
  • 送信元: Contact Form <onboarding@resend.dev>
  • 送信先: CONTACT_EMAIL 環境変数
  • 返信先: ユーザーが入力したメールアドレス
  • 件名: [お問い合わせ] ○○様からのお問い合わせ

カスタマイズ例

コード内の以下の部分を編集してください:

1. 件名の変更

subject: `【サービス名】お問い合わせ from ${safeName}`

2. 送信元の変更(ドメイン検証後)

from: `サポート <support@yourdomain.com>`

3. 本文テンプレートの変更

text: `お問い合わせありがとうございます

お名前: ${safeName}
メールアドレス: ${safeEmail}

お問い合わせ内容:
${safeMessage}

---
このメールは自動送信されています。
サポートチーム`

開発環境での注意: Resend でドメイン未検証の場合、送信元は onboarding@resend.dev のみ使用可能です。CONTACT_EMAIL をResendアカウントのメールに設定してください。

# トラブルシューティング

データベース接続に失敗する

一般的な原因:

  • .env に DATABASE_URL が未設定 / タイプミス
  • DATABASE_URLフォーマットが正しくない
  • ?sslmode=require パラメータがない
  • データベースが初期化されていない

解決方法:

# Prismaクライアントを生成(初回/スキーマ変更時)
npx prisma generate

# データベースをリセット
npx prisma migrate reset

# 注意: Prismaは .env を読み込みます(.env.local のみではNG)
認証が機能しない

確認事項:

  • Neon Auth が有効で、Configuration から環境変数をコピー済み
  • NEXT_PUBLIC_STACK_PROJECT_ID / NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY / STACK_SECRET_SERVER_KEY が設定されている
  • ブラウザでCookieが有効になっている
Stripe決済が失敗する

確認事項:

  • STRIPE_PRICE_ID が「継続・毎月」のPriceに対応している
  • 公開可能キー/シークレットキーのモード(テスト/本番)が一致
  • Webhookエンドポイントが正しく設定され、必要イベントが選択されている
  • STRIPE_WEBHOOK_SECRET が正しい(Invalid signature は取り違えのサイン)
  • ローカル: stripe listen 実行中(転送先: localhost:3002)
メールが送信されない

開発環境:

  • CONTACT_EMAIL をResendアカウントのメールに設定
  • RESEND_API_KEY が設定されている
  • Resendダッシュボードでログを確認

本番環境:

  • ドメイン検証を完了
  • SPF/DKIMレコードを確認
  • RESEND_FROM を本番ドメインの送信元に設定
ビルドエラー
# キャッシュをクリアして再インストール
rm -rf node_modules .next
npm install
npm run build