0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js + Supabase + Clerk + Stripe で月額¥170のSaaSを1人で作った技術構成

0
Posted at

はじめに

請求書から全銀フォーマットの振込データを自動生成するサービス「トルカ」を個人で開発しました。

固定費は月額約¥170(ドメイン代のみ)。あとはすべて無料枠で運用しています。

この記事では、各技術の選定理由・実装のポイント・無料枠の活用法をまとめます。同じように個人でSaaSを作りたい人の参考になれば幸いです。

全体アーキテクチャ

┌──────────────────────────────────────────────────┐
│  Vercel (Hobby Plan / 無料)                       │
│  ┌────────────────────────────────────┐           │
│  │  Next.js 16 (App Router)          │           │
│  │  ├─ Server Components             │           │
│  │  ├─ API Routes                    │           │
│  │  └─ Middleware (Clerk認証)         │           │
│  └────────────────────────────────────┘           │
│              │           │           │            │
│    ┌─────────┘     ┌─────┘     ┌─────┘            │
│    ▼               ▼           ▼                  │
│  Supabase        Clerk      Stripe               │
│  (PostgreSQL     (認証)      (決済)                │
│   + Storage)                                      │
│    ▲                                              │
│    │                                              │
│  Anthropic Claude Vision API                     │
│  (AI請求書解析)                                    │
└──────────────────────────────────────────────────┘
                    │
              Cloudflare DNS
                    │
              toruca.app

各サービスの無料枠

サービス 無料枠 実使用量
Vercel (Hobby) 100GB帯域/月, 10秒実行制限 十分余裕
Supabase (Free) 500MB DB, 1GB Storage, 50,000 Auth MAU 余裕
Clerk (Free) 10,000 MAU, Google OAuth込み 余裕
Stripe 固定費なし(決済手数料3.6%のみ) 売上比例
Cloudflare DNS, SSL, CDN 完全無料
Vercel Analytics 2,500イベント/月 初期は十分

合計固定費: ドメイン代 約¥2,000/年 ≒ ¥170/月

1. Next.js 16 — フロントエンドもAPIもこれ一本

選定理由

  • フロントエンドとバックエンドを1つのプロジェクトで管理
  • App Router + Server Components で高速表示
  • API Routes でREST APIを簡単に実装
  • Vercelとの最適な親和性

実装のポイント

Server Components でデータフェッチ

静的ページ(LP、ガイド、FAQ)は Server Components でビルド時にHTMLを生成。初回表示が高速です。

API Routes の設計

/api/upload          → 請求書アップロード + AI解析
/api/invoices        → 請求書一覧・詳細CRUD
/api/export          → 全銀フォーマット生成
/api/checkout        → Stripe Checkout
/api/webhooks/stripe → Stripe Webhook受信
/api/suppliers       → 仕入先マスタCRUD
/api/banks/search    → 銀行名検索

計20以上のAPI Routeがありますが、すべて src/app/api/ 配下に配置。

Middleware で認証チェック

// middleware.ts
export default clerkMiddleware(async (auth, request) => {
    const { pathname } = request.nextUrl;
    // 公開ルート以外は認証必須
    if (!publicRoutes.includes(pathname)) {
        await auth.protect();
    }
});

2. Supabase — DB + ストレージ + 認証(バックアップ)

PostgreSQL

Prisma ORM 経由で利用。8テーブル構成:

model User {
    id              String   @id @default(uuid())
    clerkId         String   @unique
    email           String?
    creditBalance   Int      @default(0)
    stripeCustomerId String? @unique
}

model Invoice {
    id            String   @id @default(uuid())
    userId        String
    status        String   @default("pending")
    extractedData Json?
    confirmedData Json?
    fileKey       String?
}

Prisma × Supabase の注意点

Supabase は接続プーリングに PgBouncer を使用しています。Prisma では DATABASE_URL(プーラー経由)と DIRECT_URL(直接接続、マイグレーション用)を分ける必要があります:

DATABASE_URL="postgresql://user:pass@host:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://user:pass@host:5432/postgres"

Supabase Storage

請求書ファイル(PDF/画像)の保存先。ローカル開発時はファイルシステム、本番はSupabase Storageに自動切替:

export async function uploadFile(fileKey: string, data: Buffer, contentType: string) {
    if (isSupabaseConfigured()) {
        await supabase.storage.from("invoices").upload(fileKey, data, { contentType });
    } else {
        await fs.writeFile(path.join(".uploads", fileKey), data);
    }
}

3. Clerk — 認証を10分で実装

選定理由

  • 導入の速さ: npm install @clerk/nextjs → Provider追加 → 完了
  • Google OAuth: ダッシュボードでトグルONするだけ
  • 日本語ローカライゼーション: jaJP ロケール対応
  • Webhook不要: ユーザー情報は auth() で即座に取得

実装コード

// layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
import { jaJP } from "@clerk/localizations";

export default function RootLayout({ children }) {
    return (
        <ClerkProvider localization={jaJP}>
            {children}
        </ClerkProvider>
    );
}

ユーザー同期

ClerkユーザーとDBユーザーの同期は「初回アクセス時に作成」方式:

export async function getOrCreateUser() {
    const { userId } = await auth();
    let user = await prisma.user.findUnique({ where: { clerkId: userId } });
    if (!user) {
        user = await prisma.$transaction(async (tx) => {
            const newUser = await tx.user.create({
                data: { clerkId: userId, email, creditBalance: 3 }
            });
            await tx.creditTransaction.create({
                data: { userId: newUser.id, amount: 3, type: "initial_grant" }
            });
            return newUser;
        });
    }
    return user;
}

新規登録時に3クレジット(無料お試し分)を付与しています。

4. Stripe — 決済をWebhookで完結

料金体系

プラン 件数 価格 単価
スタンダード 10件 ¥980 ¥98
プロ 50件 ¥3,980 ¥80
バリュー 100件 ¥6,980 ¥70
月額ライト 30件/月 ¥1,980 ¥66
月額スタンダード 80件/月 ¥3,980 ¥50

Checkout Session

const session = await stripe.checkout.sessions.create({
    customer: stripeCustomerId,
    line_items: [{ price: plan.priceId, quantity: 1 }],
    mode: plan.type === "one_time" ? "payment" : "subscription",
    success_url: `${baseUrl}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${baseUrl}/billing/cancel`,
});

Webhook冪等性

Stripeは同じイベントを複数回送信することがあります。stripeSessionId で重複チェック:

const existing = await prisma.creditTransaction.findFirst({
    where: { stripeSessionId: session.id }
});
if (existing) return NextResponse.json({ received: true }); // 二重処理防止

5. 全銀フォーマット生成 — レガシー仕様との格闘

全銀協フォーマットは1976年制定。Shift-JIS・120バイト固定長という仕様は現代のWeb開発とは対極にあります。

// Node.jsでShift-JISエンコード
import iconv from "iconv-lite";

function toShiftJIS(text: string): Buffer {
    return iconv.encode(text, "Shift_JIS");
}

function padRight(str: string, byteLength: number): string {
    const buf = toShiftJIS(str);
    const padding = Buffer.alloc(byteLength - buf.length, 0x20); // スペース埋め
    return iconv.decode(Buffer.concat([buf, padding]), "Shift_JIS");
}

半角カナ・全角スペース・固定長バイト計算など、テストなしでは正しく実装できない部分です。自動テスト85件で品質を担保しています。

6. インフラ運用

Supabase停止防止

Supabase Free プランは7日間アクセスがないとDBが停止します。Vercel Cronで毎日ヘルスチェック:

// vercel.json
{
    "crons": [{
        "path": "/api/health",
        "schedule": "0 0 * * *"
    }]
}

セキュリティヘッダー

// next.config.ts
headers: [
    { key: "X-Frame-Options", value: "DENY" },
    { key: "Strict-Transport-Security", value: "max-age=63072000" },
    { key: "X-Content-Type-Options", value: "nosniff" },
    { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
]

管理者機能

const ADMIN_EMAILS = process.env.ADMIN_EMAILS?.split(",") || [];

export function isAdmin(email: string): boolean {
    return ADMIN_EMAILS.includes(email);
}

環境変数でメールアドレスを指定するだけのシンプルな管理者判定。管理画面(/admin)ではユーザー管理、問い合わせ管理、システムログを確認できます。

開発効率を上げた工夫

Git不使用・Vercel CLI直接デプロイ

個人開発で初期は Git なしで Vercel CLI の vercel --prod で直接デプロイしています。ブランチ管理のオーバーヘッドがなく、開発→デプロイが1コマンド。

AI(Claude)によるペアプログラミング

実はこのサービス自体、Claude をペアプログラミングのパートナーとして開発しました。設計相談→コード生成→レビュー→デバッグのサイクルを高速に回せました。

まとめ

観点 内容
固定費 月額¥170(ドメイン代のみ)
従量費 AI解析 ¥3〜5/件 + Stripe手数料 3.6%
開発期間 約3週間(1人)
テスト 85件の自動テスト
ページ数 20以上のページ + 20以上のAPI Route

無料枠を最大限活用すれば、月額¥170でSaaSを運用できます。 利用量が増えてきたら各サービスの有料プランに移行すればよく、初期リスクを最小化できます。

個人開発でSaaSを始めたい方は、ぜひこの構成を参考にしてみてください。


サービス: トルカ 振込アシスト — 請求書からAIで全銀ファイルを自動生成

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?