はじめに
請求書から全銀フォーマットの振込データを自動生成するサービス「トルカ」を個人で開発しました。
固定費は月額約¥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で全銀ファイルを自動生成