個人開発者のAI活用SaaS構築記:3日間でプロダクションローンチ

個人開発者のAI活用SaaS構築記:3日間でプロダクションローンチ

SvelteKit、Supabase、Google Gemini APIで構築したB2B AI OCRサービスの実践開発記。技術選定理由、実装プロセス、ビジネス戦略までソロ開発者の生々しい経験談。

概要

3日間でAIベースのB2B SaaSをプロダクションまでローンチしました。Agent Effi Flowという名前のこのサービスは、免税処理OCRと経理OCRという2つのAI自動化ツールを提供し、日本のインバウンド観光ビジネスと中小企業の経理チームをターゲットにしています。

この記事は「理論的に可能だ」ではなく実際に実装した経験を共有します。技術スタック選定理由、核心実装事項、直面した課題、そして3ヶ月のKPI目標まで正直に記録しました。

なぜこのSaaSを作ったのか

課題定義

日本で働きながら発見したB2B自動化ニーズ:

  1. 免税処理業務の手作業依存: パスポートと免税書類を目視確認し手入力
  2. 経理業務の反復作業: レシートOCR後の手動データ整理
  3. 既存ソリューションの限界: 高価なエンタープライズソリューションか精度の低い汎用OCR

個人開発者としての差別化ポイントはAIを活用した構造化データ抽出です。単純なテキストOCRを超えて、Google Gemini APIのStructured Output機能で型安全なJSON応答を受け取り、即座にビジネスロジックに活用可能なデータを提供します。

技術スタック選定

SvelteKitを選んだ理由

Next.jsではなくSvelteKitを選んだ理由:

1. Svelte 5の革新的なリアクティビティシステム

// Svelte 5 Runes: $stateと$derived
let count = $state(0);
let doubled = $derived(count * 2);

// React hooksより直感的でボイラープレートコード削減

2. バンドルサイズとパフォーマンス

  • Svelteはコンパイル時にフレームワークコードを削除
  • クライアントバンドルがReact比40%小さい
  • Time to Interactiveが目に見えて速い

3. 開発者体験

  • 学習曲線が低くコードが読みやすい
  • TypeScriptサポート優秀
  • Viteベースで HMRが速い

Supabaseをバックエンドに選んだ理由

FirebaseではなくSupabase:

1. PostgreSQLの強力さ

-- Row Level Securityでマルチテナント実装
CREATE POLICY "Users can only access their own data"
ON credits FOR SELECT
USING (auth.uid() = user_id);

2. 統合された機能

  • Auth: メール/ソーシャルログイン即座に使用
  • Database: PostgreSQL with real-time subscriptions
  • Storage: ファイルアップロードとCDN
  • Edge Functions: Serverless関数 (Denoベース)

3. オープンソースと価格

  • 完全オープンソース (self-hosting可能)
  • 無料ティアが寛容 (50,000 MAU、500MB DB)
  • ベンダーロックイン心配なし

Google Gemini APIをAIモデルに選んだ理由

OpenAI GPTではなくGemini:

1. コスト効率

Gemini 2.5 Flash:
- Input: $0.075 / 1M tokens
- Output: $0.30 / 1M tokens

GPT-4 Turbo:
- Input: $10 / 1M tokens
- Output: $30 / 1M tokens

→ 約100倍安い

2. Structured Output対応

const responseSchema = {
  type: Type.OBJECT,
  required: ['store_name', 'items', 'tax', 'total_with_tax'],
  properties: {
    store_name: { type: Type.STRING },
    items: {
      type: Type.ARRAY,
      items: {
        type: Type.OBJECT,
        properties: {
          name: { type: Type.STRING },
          quantity: { type: Type.NUMBER },
          unit_price: { type: Type.NUMBER }
        }
      }
    },
    tax: { type: Type.NUMBER },
    total_with_tax: { type: Type.NUMBER }
  }
};

// レスポンスがスキーマに準拠するよう強制
const result = await model.generateContent({
  contents: [{ role: 'user', parts: [{ text: prompt }] }],
  generationConfig: {
    responseMimeType: 'application/json',
    responseSchema
  }
});

3. マルチモーダル性能

  • 画像OCR品質が優秀 (OmniDocBenchベンチマーク1位)
  • パスポート、レシートのような複雑なレイアウト処理に強み

Vercelをデプロイプラットフォームに選んだ理由

1. SvelteKit最適化

  • SvelteKitを作ったVercelが直接サポート
  • 自動SSR/Edge Functionデプロイ
  • ビルドキャッシングでデプロイ速度速い

2. Serverlessアーキテクチャ

  • APIルートが自動でserverless functionとしてデプロイ
  • 使用量ベース課金 (トラフィックなければコストほぼなし)
  • 全世界Edge Network

核心実装

1. OCR API with Structured Output

実際のコード (src/routes/api/receipt-ocr/+server.ts):

import { GoogleGenerativeAI, SchemaType as Type } from '@google/generative-ai';

const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
const model = genAI.getGenerativeModel({
  model: 'gemini-2.0-flash-exp'
});

// スキーマ定義で型安全性確保
const responseSchema = {
  type: Type.OBJECT,
  required: ['store_name', 'purchase_date', 'items', 'tax', 'total_with_tax'],
  properties: {
    store_name: {
      type: Type.STRING,
      description: '店舗名'
    },
    purchase_date: {
      type: Type.STRING,
      description: 'YYYY-MM-DD形式の購入日'
    },
    items: {
      type: Type.ARRAY,
      items: {
        type: Type.OBJECT,
        properties: {
          name: { type: Type.STRING },
          quantity: { type: Type.NUMBER },
          unit_price: { type: Type.NUMBER },
          total_price: { type: Type.NUMBER }
        }
      }
    },
    subtotal: { type: Type.NUMBER },
    tax: { type: Type.NUMBER },
    total_with_tax: { type: Type.NUMBER }
  }
};

// 画像をbase64にエンコード
const imageBase64 = await fileToBase64(imageFile);

const result = await model.generateContent({
  contents: [
    {
      role: 'user',
      parts: [
        {
          inlineData: {
            mimeType: imageFile.type,
            data: imageBase64
          }
        },
        {
          text: `次のレシート画像を分析して構造化JSONで返してください。

          要件:
          1. 店舗名、購入日、品目リスト抽出
          2. 各品目の名前、数量、単価、合計
          3. 消費税と最終合計
          4. 日付はYYYY-MM-DD形式に変換
          `
        }
      ]
    }
  ],
  generationConfig: {
    responseMimeType: 'application/json',
    responseSchema
  }
});

const parsedData = JSON.parse(result.response.text());
// parsedDataは既にスキーマに準拠する型安全オブジェクト

核心メリット:

  • 型安全性: スキーマでレスポンス構造強制
  • パースエラー除去: JSONパース失敗ほぼなし
  • 即活用可能: DB保存やAPIレスポンスに直接使用

2. Credit System (クレジットシステム)

Stripe Checkout統合:

// src/routes/agents/credits/+page.server.ts
import Stripe from 'stripe';

const stripe = new Stripe(STRIPE_SECRET_KEY);

export const actions = {
  purchase: async ({ request, locals }) => {
    const data = await request.formData();
    const planId = data.get('plan_id');

    const plans = {
      starter: { price: 2000, credits: 1000 },
      pro: { price: 10000, credits: 5500 },
      business: { price: 40000, credits: 23000 }
    };

    const selectedPlan = plans[planId];

    // Stripe Checkoutセッション生成
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [
        {
          price_data: {
            currency: 'jpy',
            product_data: {
              name: `${planId.toUpperCase()} プラン - ${selectedPlan.credits} クレジット`
            },
            unit_amount: selectedPlan.price
          },
          quantity: 1
        }
      ],
      mode: 'payment',
      success_url: `${url.origin}/credits/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${url.origin}/credits`,
      metadata: {
        user_id: locals.user.id,
        credits: selectedPlan.credits,
        plan_id: planId
      }
    });

    return { session_url: session.url };
  }
};

Webhookでクレジット付与:

// src/routes/api/webhooks/stripe/+server.ts
export const POST = async ({ request }) => {
  const signature = request.headers.get('stripe-signature');
  const body = await request.text();

  // Stripe webhook検証
  const event = stripe.webhooks.constructEvent(
    body,
    signature,
    STRIPE_WEBHOOK_SECRET
  );

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    const { user_id, credits } = session.metadata;

    // Supabaseにクレジット追加
    await supabase
      .from('credits')
      .insert({
        user_id,
        amount: parseInt(credits),
        type: 'purchase',
        description: `Purchased ${credits} credits`
      });
  }

  return new Response(JSON.stringify({ received: true }));
};

3. API Authentication (API認証)

APIキー発行と検証:

// APIキー生成
export const actions = {
  createApiKey: async ({ locals }) => {
    const apiKey = `effi_${crypto.randomUUID()}`;

    await supabase
      .from('api_keys')
      .insert({
        user_id: locals.user.id,
        key: apiKey,
        created_at: new Date().toISOString()
      });

    return { apiKey };
  }
};

// APIキー検証 (すべてのAPIリクエストで実行)
async function validateApiKey(request: Request) {
  const apiKey = request.headers.get('X-API-Key');

  if (!apiKey) {
    throw error(401, 'API key required');
  }

  const { data: keyData } = await supabase
    .from('api_keys')
    .select('user_id, is_active')
    .eq('key', apiKey)
    .single();

  if (!keyData || !keyData.is_active) {
    throw error(401, 'Invalid or inactive API key');
  }

  return keyData.user_id;
}

// クレジット控除前残高確認
async function checkAndDeductCredits(userId: string, amount: number) {
  const { data: balance } = await supabase
    .rpc('get_credit_balance', { user_id: userId });

  if (balance < amount) {
    throw error(402, 'Insufficient credits');
  }

  await supabase
    .from('credits')
    .insert({
      user_id: userId,
      amount: -amount,
      type: 'usage',
      description: 'OCR API call'
    });
}

4. Multi-language Support (多言語対応)

Paraglideを使ったi18n:

// src/lib/i18n.ts
import { paraglide } from '@inlang/paraglide-sveltekit';

export const i18n = paraglide({
  project: './project.inlang',
  outdir: './src/paraglide',
  defaultLocale: 'ja'
});

// サポート言語: ko, en, ja, zh, es

言語別ルーティング:

// src/routes/[lang]/+layout.server.ts
export const load = async ({ params }) => {
  const lang = params.lang || 'ja';

  return {
    lang,
    translations: await import(`../../locales/${lang}.json`)
  };
};

トークン使用量追跡とコスト最適化

リアルタイムトークンモニタリング:

const result = await model.generateContent({...});

// Gemini APIはトークン使用量を返す
const usage = result.response.usageMetadata;

await supabase
  .from('api_usage')
  .insert({
    user_id,
    input_tokens: usage.promptTokenCount,
    output_tokens: usage.candidatesTokenCount,
    total_tokens: usage.totalTokenCount,
    estimated_cost: calculateCost(usage)
  });

function calculateCost(usage) {
  const INPUT_COST = 0.075 / 1_000_000; // $0.075 per 1M tokens
  const OUTPUT_COST = 0.30 / 1_000_000; // $0.30 per 1M tokens

  return (
    usage.promptTokenCount * INPUT_COST +
    usage.candidatesTokenCount * OUTPUT_COST
  );
}

ビジネス戦略

Target Customers (ターゲット顧客)

  1. 日本インバウンド観光ビジネス

    • 免税店、ドラッグストア、百貨店
    • 月1,000件以上の免税処理
  2. 中小企業経理チーム

    • 月500件以上のレシート処理
    • 会計ソフト連携ニーズ
  3. 税理士事務所

    • 顧客企業の経理代行
    • 正確なデータ必要

SEO/AEO-Driven Acquisition (検索ベース顧客獲得)

コンテンツマーケティング戦略:

  1. jangwook.netブログ活用

    • 技術ブログを通じたブランド信頼度構築
    • OCR、AI自動化関連キーワードターゲティング
    • この記事もその一環
  2. 日本語キーワード最適化

    • “免税 OCR”
    • “経理 自動化”
    • “領収書 AI”
  3. FAQページでAEO対応

    • “OCR精度は?” → 構造化された回答
    • “API連携方法は?” → 開発者ドキュメント
    • “価格は?” → 透明な価格表

3-Month KPI Targets (3ヶ月KPI目標)

現実的なソロ開発者目標:

指標目標測定方法
月間訪問者500+Google Analytics
会員登録30名Supabase Authテーブル
有料転換5名Stripe Dashboard
MRR¥30,000Stripe購読合計
OCR API呼び出し1,000回api_usageテーブル

Why These Numbers?

  • 500訪問者 → SEOで達成可能な現実的数値
  • 6% 転換率 (30/500) → B2B SaaS平均
  • 16.7% 有料転換 (5/30) → プレミアムでターゲティング
  • ¥6,000 ARPU → 中小企業予算に適合

開発スケジュール

Day 1 (2025-11-24): Foundation

完了項目:

  • プロジェクト初期化 (SvelteKit + TypeScript)
  • Supabase連携 (Auth + Database)
  • 最初のサービス実装: Receipt OCR for Tax Refund
  • パスポート + 免税書類自動認識
  • Structured Outputスキーマ検証

コード作成量: 〜800 lines

Day 2 (2025-11-25): Payment & Second Service

完了項目:

  • Accounting OCRサービス追加
  • Stripe Checkout統合
  • クレジットシステム実装
  • Webhookで自動チャージ
  • 法的ページ作成
    • 特定商取引法
    • プライバシーポリシー
    • 利用規約
  • Google Analytics連携

コード作成量: 〜1,200 lines

Day 3 (2025-11-26): Polish & Launch

完了項目:

  • サービス説明ページ
  • APIドキュメント作成
  • ランディングページ最適化
  • プロダクションデプロイ (Vercel)
  • DNS設定

コード作成量: 〜600 lines

合計: 3日、〜2,600 lines of code

学んだこと

1. SvelteKit 5のReactivityはゲームチェンジャー

Before (React hooks):

const [credits, setCredits] = useState(0);

useEffect(() => {
  const doubled = credits * 2;
  setDoubled(doubled);
}, [credits]);

After (Svelte 5 runes):

let credits = $state(0);
let doubled = $derived(credits * 2);
// 自動でリアクティビティ処理、useEffect不要

実感したメリット:

  • ボイラープレートコード70%削減
  • デバッグが簡単 (明示的依存性不要)
  • パフォーマンス向上 (不要な再レンダリングなし)

2. Supabase RLSでマルチテナントが簡単に

Row Level Securityポリシー:

-- 各ユーザーは自分のクレジットのみ照会可能
CREATE POLICY "Users can view own credits"
ON credits FOR SELECT
USING (auth.uid() = user_id);

-- クレジット控除はサーバーのみ
CREATE POLICY "Only service role can deduct"
ON credits FOR INSERT
USING (auth.role() = 'service_role');

メリット:

  • アプリケーションコードで権限チェック不要
  • SQLレベルでデータ隔離保証
  • セキュリティ脆弱性根本遮断

3. Gemini APIコスト最適化

プロンプト最適化:

// Before: 長いプロンプト
const prompt = `
あなたは専門OCRシステムです。
次の画像を分析して...
(500字以上の指示事項)
`;
// → Input tokens: ~150

// After: 簡潔なプロンプト
const prompt = `Extract receipt data as JSON:
- store_name, date, items[], tax, total`;
// → Input tokens: ~25

// コスト削減: 83%

画像サイズ最適化:

// 画像を1024px以内にリサイズ
import sharp from 'sharp';

const optimized = await sharp(imageBuffer)
  .resize(1024, 1024, { fit: 'inside' })
  .png({ quality: 80 })
  .toBuffer();

// トークン使用量: 70%削減

4. Solo Developer Productivity Tips

生産性を最大化した方法:

  1. Supabase CLIでローカル開発

    supabase start
    # ローカルPostgreSQL + Auth + Storage
    # プロダクションと同じ環境
  2. Claude Codeでボイラープレート自動生成

    • CRUD APIスキャフォールディング
    • TypeScript型定義
    • Zodスキーマ検証
  3. Vercel Preview Deployments

    • PRごとに自動デプロイURL
    • クライアントに即座にデモ可能

次のステップ

即時実行 (1週間以内)

  1. 日本語キーワードSEO最適化

    • Meta description日本語で作成
    • Open Graph画像最適化
    • Schema.orgマークアップ追加
  2. FAQページ作成

    • “OCR精度は?” → 実際のデータで回答
    • “どんなレシート形式対応?” → サンプル画像
    • “API制限は?” → Rate limit明示
  3. Google Search Console提出

    • サイトマップ登録
    • インデックスリクエスト

短期目標 (1ヶ月以内)

  1. 新サービス追加

    • ビジネス文書処理の拡大
  2. 最初のB2B顧客獲得

    • 免税店1箇所契約目標
    • クレジットベース課金モデル
  3. APIドキュメント高度化

    • OpenAPIスペック作成
    • Postman Collection提供
    • SDKサンプルコード (Python、Node.js)

中期目標 (3ヶ月以内)

  1. MRR ¥30,000達成

    • 5名有料顧客
    • 月500回API呼び出し
  2. 使用事例共有

    • 顧客成功事例ブログポスト
    • 実際の活用事例ドキュメント化
  3. プレミアム機能追加

    • Batch processing (一括処理)
    • Custom schema support
    • Webhook integration

参考資料

Official Documentation

Key Articles Referenced

結びの言葉

3日間でプロダクションSaaSをローンチすることは可能です。しかし「速く作る」が目標ではなく「速く検証する」が目標であるべきです。

核心は次の3つでした:

  1. 適切なツール選択: SvelteKit + Supabase + Gemini API
  2. スコープ制限: 2サービスでスタート、完璧さよりローンチ優先
  3. ビジネス優先: 技術より顧客課題解決に集中

今、本当の旅が始まります。最初の有料顧客を獲得し、フィードバックを受け、改善するプロセス。3ヶ月後この記事を更新する時「MRR ¥30,000達成」と書けることを願います。

ソロ開発者の皆さん、一緒に作りましょう。

他の言語で読む

この記事は役に立ちましたか?

より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください!☕

著者について

JK

Kim Jangwook

AI/LLM専門フルスタック開発者

10年以上のWeb開発経験を活かし、AIエージェントシステム、LLMアプリケーション、自動化ソリューションを構築しています。Claude Code、MCP、RAGシステムの実践的な知見を共有します。